cli/pkg/cmd/issue/create/create_test.go
Kynan Ware 201eb7ec5e Merge issuesV2Flags cases into TestNewCmdCreate
The five Issues 2.0 cases (type, parent by number, parent by URL,
blocked-by, blocking) live as a separate parallel test today. They
exercise the same NewCmdCreate code path as the existing cases, so
fold them into TestNewCmdCreate and extend the assertions to cover
IssueType, Parent, BlockedBy, and Blocking.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-05-06 16:22:45 -06:00

1732 lines
50 KiB
Go

package create
import (
"bytes"
"encoding/json"
"fmt"
"io"
"net/http"
"os"
"path/filepath"
"strings"
"testing"
"github.com/MakeNowJust/heredoc"
"github.com/cli/cli/v2/internal/browser"
"github.com/cli/cli/v2/internal/config"
fd "github.com/cli/cli/v2/internal/featuredetection"
"github.com/cli/cli/v2/internal/gh"
"github.com/cli/cli/v2/internal/ghrepo"
"github.com/cli/cli/v2/internal/prompter"
"github.com/cli/cli/v2/internal/run"
prShared "github.com/cli/cli/v2/pkg/cmd/pr/shared"
"github.com/cli/cli/v2/pkg/cmdutil"
"github.com/cli/cli/v2/pkg/httpmock"
"github.com/cli/cli/v2/pkg/iostreams"
"github.com/cli/cli/v2/test"
"github.com/google/shlex"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestNewCmdCreate(t *testing.T) {
tmpFile := filepath.Join(t.TempDir(), "my-body.md")
err := os.WriteFile(tmpFile, []byte("a body from file"), 0600)
require.NoError(t, err)
tests := []struct {
name string
tty bool
stdin string
cli string
config string
wantsErr bool
wantsOpts CreateOptions
}{
{
name: "empty non-tty",
tty: false,
cli: "",
wantsErr: true,
},
{
name: "only title non-tty",
tty: false,
cli: "-t mytitle",
wantsErr: true,
},
{
name: "empty tty",
tty: true,
cli: "",
wantsErr: false,
wantsOpts: CreateOptions{
Title: "",
Body: "",
RecoverFile: "",
WebMode: false,
Interactive: true,
},
},
{
name: "body from stdin",
tty: false,
stdin: "this is on standard input",
cli: "-t mytitle -F -",
wantsErr: false,
wantsOpts: CreateOptions{
Title: "mytitle",
Body: "this is on standard input",
RecoverFile: "",
WebMode: false,
Interactive: false,
},
},
{
name: "body from file",
tty: false,
cli: fmt.Sprintf("-t mytitle -F '%s'", tmpFile),
wantsErr: false,
wantsOpts: CreateOptions{
Title: "mytitle",
Body: "a body from file",
RecoverFile: "",
WebMode: false,
Interactive: false,
},
},
{
name: "template from name tty",
tty: true,
cli: `-t mytitle --template "bug report"`,
wantsErr: false,
wantsOpts: CreateOptions{
Title: "mytitle",
Body: "",
RecoverFile: "",
WebMode: false,
Template: "bug report",
Interactive: true,
},
},
{
name: "template from name non-tty",
tty: false,
cli: `-t mytitle --template "bug report"`,
wantsErr: true,
},
{
name: "template and body",
tty: false,
cli: `-t mytitle --template "bug report" --body "issue body"`,
wantsErr: true,
},
{
name: "template and body file",
tty: false,
cli: `-t mytitle --template "bug report" --body-file "body_file.md"`,
wantsErr: true,
},
{
name: "editor by cli",
tty: true,
cli: "--editor",
wantsErr: false,
wantsOpts: CreateOptions{
Title: "",
Body: "",
RecoverFile: "",
WebMode: false,
EditorMode: true,
Interactive: false,
},
},
{
name: "editor by config",
tty: true,
cli: "",
config: "prefer_editor_prompt: enabled",
wantsErr: false,
wantsOpts: CreateOptions{
Title: "",
Body: "",
RecoverFile: "",
WebMode: false,
EditorMode: true,
Interactive: false,
},
},
{
name: "editor and template",
tty: true,
cli: `--editor --template "bug report"`,
wantsErr: false,
wantsOpts: CreateOptions{
Title: "",
Body: "",
RecoverFile: "",
WebMode: false,
EditorMode: true,
Template: "bug report",
Interactive: false,
},
},
{
name: "editor and web",
tty: true,
cli: "--editor --web",
wantsErr: true,
},
{
name: "can use web even though editor is enabled by config",
tty: true,
cli: `--web --title mytitle --body "issue body"`,
config: "prefer_editor_prompt: enabled",
wantsErr: false,
wantsOpts: CreateOptions{
Title: "mytitle",
Body: "issue body",
RecoverFile: "",
WebMode: true,
EditorMode: false,
Interactive: false,
},
},
{
name: "editor with non-tty",
tty: false,
cli: "--editor",
wantsErr: true,
},
{
name: "type",
tty: false,
cli: `-t mytitle -b mybody --type Bug`,
wantsErr: false,
wantsOpts: CreateOptions{
Title: "mytitle",
Body: "mybody",
IssueType: "Bug",
},
},
{
name: "parent by number",
tty: false,
cli: `-t mytitle -b mybody --parent 100`,
wantsErr: false,
wantsOpts: CreateOptions{
Title: "mytitle",
Body: "mybody",
Parent: "100",
},
},
{
name: "parent by URL",
tty: false,
cli: `-t mytitle -b mybody --parent https://github.com/cli/go-gh/issues/42`,
wantsErr: false,
wantsOpts: CreateOptions{
Title: "mytitle",
Body: "mybody",
Parent: "https://github.com/cli/go-gh/issues/42",
},
},
{
name: "blocked by multiple issues",
tty: false,
cli: `-t mytitle -b mybody --blocked-by 200,201`,
wantsErr: false,
wantsOpts: CreateOptions{
Title: "mytitle",
Body: "mybody",
BlockedBy: []string{"200", "201"},
},
},
{
name: "blocking another issue",
tty: false,
cli: `-t mytitle -b mybody --blocking 300`,
wantsErr: false,
wantsOpts: CreateOptions{
Title: "mytitle",
Body: "mybody",
Blocking: []string{"300"},
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
ios, stdin, stdout, stderr := iostreams.Test()
if tt.stdin != "" {
_, _ = stdin.WriteString(tt.stdin)
} else if tt.tty {
ios.SetStdinTTY(true)
ios.SetStdoutTTY(true)
}
f := &cmdutil.Factory{
IOStreams: ios,
Config: func() (gh.Config, error) {
if tt.config != "" {
return config.NewFromString(tt.config), nil
}
return config.NewBlankConfig(), nil
},
}
var opts *CreateOptions
cmd := NewCmdCreate(f, func(o *CreateOptions) error {
opts = o
return nil
})
args, err := shlex.Split(tt.cli)
require.NoError(t, err)
cmd.SetArgs(args)
cmd.SetOut(io.Discard)
cmd.SetErr(io.Discard)
_, err = cmd.ExecuteC()
if tt.wantsErr {
assert.Error(t, err)
return
} else {
require.NoError(t, err)
}
assert.Equal(t, "", stdout.String())
assert.Equal(t, "", stderr.String())
assert.Equal(t, tt.wantsOpts.Body, opts.Body)
assert.Equal(t, tt.wantsOpts.Title, opts.Title)
assert.Equal(t, tt.wantsOpts.RecoverFile, opts.RecoverFile)
assert.Equal(t, tt.wantsOpts.WebMode, opts.WebMode)
assert.Equal(t, tt.wantsOpts.Interactive, opts.Interactive)
assert.Equal(t, tt.wantsOpts.Template, opts.Template)
assert.Equal(t, tt.wantsOpts.IssueType, opts.IssueType)
assert.Equal(t, tt.wantsOpts.Parent, opts.Parent)
assert.Equal(t, tt.wantsOpts.BlockedBy, opts.BlockedBy)
assert.Equal(t, tt.wantsOpts.Blocking, opts.Blocking)
})
}
}
func Test_createRun(t *testing.T) {
tests := []struct {
name string
opts CreateOptions
httpStubs func(*httpmock.Registry)
promptStubs func(*prompter.PrompterMock)
wantsStdout string
wantsStderr string
wantsBrowse string
wantsErr string
}{
{
name: "no args",
opts: CreateOptions{
Detector: &fd.EnabledDetectorMock{},
WebMode: true,
},
wantsBrowse: "https://github.com/OWNER/REPO/issues/new",
wantsStderr: "Opening https://github.com/OWNER/REPO/issues/new in your browser.\n",
},
{
name: "title and body",
opts: CreateOptions{
Detector: &fd.EnabledDetectorMock{},
WebMode: true,
Title: "myissue",
Body: "hello cli",
},
wantsBrowse: "https://github.com/OWNER/REPO/issues/new?body=hello+cli&title=myissue",
wantsStderr: "Opening https://github.com/OWNER/REPO/issues/new in your browser.\n",
},
{
name: "assignee",
opts: CreateOptions{
Detector: &fd.EnabledDetectorMock{},
WebMode: true,
Assignees: []string{"monalisa"},
},
wantsBrowse: "https://github.com/OWNER/REPO/issues/new?assignees=monalisa&body=",
wantsStderr: "Opening https://github.com/OWNER/REPO/issues/new in your browser.\n",
},
{
name: "@me",
opts: CreateOptions{
Detector: &fd.EnabledDetectorMock{},
WebMode: true,
Assignees: []string{"@me"},
},
httpStubs: func(r *httpmock.Registry) {
r.Register(
httpmock.GraphQL(`query UserCurrent\b`),
httpmock.StringResponse(`
{ "data": {
"viewer": { "login": "MonaLisa" }
} }`))
},
wantsBrowse: "https://github.com/OWNER/REPO/issues/new?assignees=MonaLisa&body=",
wantsStderr: "Opening https://github.com/OWNER/REPO/issues/new in your browser.\n",
},
{
name: "@copilot",
opts: CreateOptions{
Detector: &fd.EnabledDetectorMock{},
WebMode: true,
Assignees: []string{"@copilot"},
},
wantsBrowse: "https://github.com/OWNER/REPO/issues/new?assignees=Copilot&body=",
wantsStderr: "Opening https://github.com/OWNER/REPO/issues/new in your browser.\n",
},
{
name: "project",
opts: CreateOptions{
Detector: &fd.EnabledDetectorMock{},
WebMode: true,
Projects: []string{"cleanup"},
},
httpStubs: func(r *httpmock.Registry) {
r.Register(
httpmock.GraphQL(`query RepositoryProjectList\b`),
httpmock.StringResponse(`
{ "data": { "repository": { "projects": {
"nodes": [
{ "name": "Cleanup", "id": "CLEANUPID", "resourcePath": "/OWNER/REPO/projects/1" }
],
"pageInfo": { "hasNextPage": false }
} } } }`))
r.Register(
httpmock.GraphQL(`query OrganizationProjectList\b`),
httpmock.StringResponse(`
{ "data": { "organization": { "projects": {
"nodes": [
{ "name": "Triage", "id": "TRIAGEID", "resourcePath": "/orgs/ORG/projects/1" }
],
"pageInfo": { "hasNextPage": false }
} } } }`))
r.Register(
httpmock.GraphQL(`query RepositoryProjectV2List\b`),
httpmock.StringResponse(`
{ "data": { "repository": { "projectsV2": {
"nodes": [
{ "title": "CleanupV2", "id": "CLEANUPV2ID", "resourcePath": "/OWNER/REPO/projects/2" }
],
"pageInfo": { "hasNextPage": false }
} } } }`))
r.Register(
httpmock.GraphQL(`query OrganizationProjectV2List\b`),
httpmock.StringResponse(`
{ "data": { "organization": { "projectsV2": {
"nodes": [
{ "title": "Triage", "id": "TRIAGEID", "resourcePath": "/orgs/ORG/projects/2" }
],
"pageInfo": { "hasNextPage": false }
} } } }`))
r.Register(
httpmock.GraphQL(`query UserProjectV2List\b`),
httpmock.StringResponse(`
{ "data": { "viewer": { "projectsV2": {
"nodes": [
{ "title": "Monalisa", "id": "MONALISAID", "resourcePath": "/users/MONALISA/projects/1" }
],
"pageInfo": { "hasNextPage": false }
} } } }`))
},
wantsBrowse: "https://github.com/OWNER/REPO/issues/new?body=&projects=OWNER%2FREPO%2F1",
wantsStderr: "Opening https://github.com/OWNER/REPO/issues/new in your browser.\n",
},
{
name: "has templates",
opts: CreateOptions{
Detector: &fd.EnabledDetectorMock{},
WebMode: true,
},
httpStubs: func(r *httpmock.Registry) {
r.Register(
httpmock.GraphQL(`query IssueTemplates\b`),
httpmock.StringResponse(`
{ "data": { "repository": { "issueTemplates": [
{ "name": "Bug report",
"body": "Does not work :((" },
{ "name": "Submit a request",
"body": "I have a suggestion for an enhancement" }
] } } }`),
)
},
wantsBrowse: "https://github.com/OWNER/REPO/issues/new/choose",
wantsStderr: "Opening https://github.com/OWNER/REPO/issues/new/choose in your browser.\n",
},
{
name: "too long body",
opts: CreateOptions{
Detector: &fd.EnabledDetectorMock{},
WebMode: true,
Body: strings.Repeat("A", 9216),
},
wantsErr: "cannot open in browser: maximum URL length exceeded",
},
{
name: "editor",
httpStubs: func(r *httpmock.Registry) {
r.Register(
httpmock.GraphQL(`query IssueRepositoryInfo\b`),
httpmock.StringResponse(`
{ "data": { "repository": {
"id": "REPOID",
"hasIssuesEnabled": true
} } }`))
r.Register(
httpmock.GraphQL(`mutation IssueCreate\b`),
httpmock.GraphQLMutation(`
{ "data": { "createIssue": { "issue": {
"URL": "https://github.com/OWNER/REPO/issues/12"
} } } }
`, func(inputs map[string]interface{}) {
assert.Equal(t, "title", inputs["title"])
assert.Equal(t, "body", inputs["body"])
}))
},
opts: CreateOptions{
Detector: &fd.EnabledDetectorMock{},
EditorMode: true,
TitledEditSurvey: func(string, string) (string, string, error) { return "title", "body", nil },
},
wantsStdout: "https://github.com/OWNER/REPO/issues/12\n",
wantsStderr: "\nCreating issue in OWNER/REPO\n\n",
},
{
name: "editor and template",
httpStubs: func(r *httpmock.Registry) {
r.Register(
httpmock.GraphQL(`query IssueRepositoryInfo\b`),
httpmock.StringResponse(`
{ "data": { "repository": {
"id": "REPOID",
"hasIssuesEnabled": true
} } }`))
r.Register(
httpmock.GraphQL(`query IssueTemplates\b`),
httpmock.StringResponse(`
{ "data": { "repository": { "issueTemplates": [
{ "name": "Bug report",
"title": "bug: ",
"body": "Does not work :((" }
] } } }`),
)
r.Register(
httpmock.GraphQL(`mutation IssueCreate\b`),
httpmock.GraphQLMutation(`
{ "data": { "createIssue": { "issue": {
"URL": "https://github.com/OWNER/REPO/issues/12"
} } } }
`, func(inputs map[string]interface{}) {
assert.Equal(t, "bug: ", inputs["title"])
assert.Equal(t, "Does not work :((", inputs["body"])
}))
},
opts: CreateOptions{
Detector: &fd.EnabledDetectorMock{},
EditorMode: true,
Template: "Bug report",
TitledEditSurvey: func(title string, body string) (string, string, error) { return title, body, nil },
},
wantsStdout: "https://github.com/OWNER/REPO/issues/12\n",
wantsStderr: "\nCreating issue in OWNER/REPO\n\n",
},
{
name: "interactive prompts with actor assignee display names when actors available",
opts: CreateOptions{
Interactive: true,
Detector: &fd.EnabledDetectorMock{},
Title: "test `gh issue create` actor assignees",
Body: "Actor assignees allow users and bots to be assigned to issues",
},
promptStubs: func(pm *prompter.PrompterMock) {
firstConfirmSubmission := true
pm.InputFunc = func(message, defaultValue string) (string, error) {
switch message {
default:
return "", fmt.Errorf("unexpected input prompt: %s", message)
}
}
pm.MultiSelectFunc = func(message string, defaults []string, options []string) ([]int, error) {
switch message {
case "What would you like to add?":
return prompter.IndexesFor(options, "Assignees")
default:
return nil, fmt.Errorf("unexpected multi-select prompt: %s", message)
}
}
pm.MultiSelectWithSearchFunc = func(message, searchPrompt string, defaults, persistentOptions []string, searchFunc func(string) prompter.MultiSelectSearchResult) ([]string, error) {
switch message {
case "Assignees":
return []string{"copilot-swe-agent", "MonaLisa"}, nil
default:
return nil, fmt.Errorf("unexpected multi-select-with-search prompt: %s", message)
}
}
pm.SelectFunc = func(message, defaultValue string, options []string) (int, error) {
switch message {
case "What's next?":
if firstConfirmSubmission {
firstConfirmSubmission = false
return prompter.IndexFor(options, "Add metadata")
}
return prompter.IndexFor(options, "Submit")
default:
return 0, fmt.Errorf("unexpected select prompt: %s", message)
}
}
},
httpStubs: func(r *httpmock.Registry) {
r.Register(
httpmock.GraphQL(`query IssueRepositoryInfo\b`),
httpmock.StringResponse(`
{ "data": { "repository": {
"id": "REPOID",
"hasIssuesEnabled": true,
"viewerPermission": "WRITE"
} } }
`))
r.Register(
httpmock.GraphQL(`mutation IssueCreate\b`),
httpmock.GraphQLMutation(`
{ "data": { "createIssue": { "issue": {
"id": "ISSUEID",
"URL": "https://github.com/OWNER/REPO/issues/12"
} } } }
`, func(inputs map[string]interface{}) {
if v, ok := inputs["assigneeIds"]; ok {
t.Errorf("did not expect assigneeIds: %v", v)
}
}))
r.Register(
httpmock.GraphQL(`mutation ReplaceActorsForAssignable\b`),
httpmock.GraphQLMutation(`
{ "data": { "replaceActorsForAssignable": { "__typename": "" } } }
`, func(inputs map[string]interface{}) {
assert.Equal(t, "ISSUEID", inputs["assignableId"])
assert.Equal(t, []interface{}{"copilot-swe-agent[bot]", "MonaLisa"}, inputs["actorLogins"])
}))
},
wantsStdout: "https://github.com/OWNER/REPO/issues/12\n",
wantsStderr: "\nCreating issue in OWNER/REPO\n\n",
},
{
name: "interactive prompts with user assignee logins when actors unavailable",
opts: CreateOptions{
Interactive: true,
Detector: &fd.DisabledDetectorMock{},
Title: "test `gh issue create` user assignees",
Body: "User assignees allow only users to be assigned to issues",
},
promptStubs: func(pm *prompter.PrompterMock) {
firstConfirmSubmission := true
pm.InputFunc = func(message, defaultValue string) (string, error) {
switch message {
default:
return "", fmt.Errorf("unexpected input prompt: %s", message)
}
}
pm.MultiSelectFunc = func(message string, defaults []string, options []string) ([]int, error) {
switch message {
case "What would you like to add?":
return prompter.IndexesFor(options, "Assignees")
case "Assignees":
return prompter.IndexesFor(options, "hubot", "MonaLisa (Mona Display Name)")
default:
return nil, fmt.Errorf("unexpected multi-select prompt: %s", message)
}
}
pm.SelectFunc = func(message, defaultValue string, options []string) (int, error) {
switch message {
case "What's next?":
if firstConfirmSubmission {
firstConfirmSubmission = false
return prompter.IndexFor(options, "Add metadata")
}
return prompter.IndexFor(options, "Submit")
default:
return 0, fmt.Errorf("unexpected select prompt: %s", message)
}
}
},
httpStubs: func(r *httpmock.Registry) {
r.Register(
httpmock.GraphQL(`query IssueRepositoryInfo\b`),
httpmock.StringResponse(`
{ "data": { "repository": {
"id": "REPOID",
"hasIssuesEnabled": true,
"viewerPermission": "WRITE"
} } }
`))
r.Register(
httpmock.GraphQL(`query RepositoryAssignableUsers\b`),
httpmock.StringResponse(`
{ "data": { "repository": { "assignableUsers": {
"nodes": [
{ "login": "hubot", "id": "HUBOTID", "name": "" },
{ "login": "MonaLisa", "id": "MONAID", "name": "Mona Display Name" }
],
"pageInfo": { "hasNextPage": false }
} } } }
`))
r.Register(
httpmock.GraphQL(`mutation IssueCreate\b`),
httpmock.GraphQLMutation(`
{ "data": { "createIssue": { "issue": {
"URL": "https://github.com/OWNER/REPO/issues/12"
} } } }
`, func(inputs map[string]interface{}) {
assert.Equal(t, []interface{}{"HUBOTID", "MONAID"}, inputs["assigneeIds"])
}))
},
wantsStdout: "https://github.com/OWNER/REPO/issues/12\n",
wantsStderr: "\nCreating issue in OWNER/REPO\n\n",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
httpReg := &httpmock.Registry{}
defer httpReg.Verify(t)
if tt.httpStubs != nil {
tt.httpStubs(httpReg)
}
ios, _, stdout, stderr := iostreams.Test()
ios.SetStdoutTTY(true)
opts := &tt.opts
opts.IO = ios
opts.HttpClient = func() (*http.Client, error) {
return &http.Client{Transport: httpReg}, nil
}
opts.BaseRepo = func() (ghrepo.Interface, error) {
return ghrepo.New("OWNER", "REPO"), nil
}
browser := &browser.Stub{}
opts.Browser = browser
prompterMock := &prompter.PrompterMock{}
opts.Prompter = prompterMock
if tt.promptStubs != nil {
tt.promptStubs(prompterMock)
}
err := createRun(opts)
if tt.wantsErr == "" {
require.NoError(t, err)
} else {
assert.EqualError(t, err, tt.wantsErr)
return
}
assert.Equal(t, tt.wantsStdout, stdout.String())
assert.Equal(t, tt.wantsStderr, stderr.String())
browser.Verify(t, tt.wantsBrowse)
})
}
}
/*** LEGACY TESTS ***/
func runCommand(rt http.RoundTripper, isTTY bool, cli string, pm *prompter.PrompterMock) (*test.CmdOut, error) {
return runCommandWithRootDirOverridden(rt, isTTY, cli, "", pm)
}
func runCommandWithRootDirOverridden(rt http.RoundTripper, isTTY bool, cli string, rootDir string, pm *prompter.PrompterMock) (*test.CmdOut, error) {
ios, _, stdout, stderr := iostreams.Test()
ios.SetStdoutTTY(isTTY)
ios.SetStdinTTY(isTTY)
ios.SetStderrTTY(isTTY)
browser := &browser.Stub{}
factory := &cmdutil.Factory{
IOStreams: ios,
HttpClient: func() (*http.Client, error) {
return &http.Client{Transport: rt}, nil
},
Config: func() (gh.Config, error) {
return config.NewBlankConfig(), nil
},
BaseRepo: func() (ghrepo.Interface, error) {
return ghrepo.New("OWNER", "REPO"), nil
},
Browser: browser,
Prompter: pm,
}
cmd := NewCmdCreate(factory, func(opts *CreateOptions) error {
opts.RootDirOverride = rootDir
opts.Detector = &fd.EnabledDetectorMock{}
return createRun(opts)
})
argv, err := shlex.Split(cli)
if err != nil {
return nil, err
}
cmd.SetArgs(argv)
cmd.SetIn(&bytes.Buffer{})
cmd.SetOut(io.Discard)
cmd.SetErr(io.Discard)
_, err = cmd.ExecuteC()
return &test.CmdOut{
OutBuf: stdout,
ErrBuf: stderr,
BrowsedURL: browser.BrowsedURL(),
}, err
}
func TestIssueCreate(t *testing.T) {
http := &httpmock.Registry{}
defer http.Verify(t)
http.Register(
httpmock.GraphQL(`query IssueRepositoryInfo\b`),
httpmock.StringResponse(`
{ "data": { "repository": {
"id": "REPOID",
"hasIssuesEnabled": true
} } }`),
)
http.Register(
httpmock.GraphQL(`mutation IssueCreate\b`),
httpmock.GraphQLMutation(`
{ "data": { "createIssue": { "issue": {
"URL": "https://github.com/OWNER/REPO/issues/12"
} } } }`,
func(inputs map[string]interface{}) {
assert.Equal(t, inputs["repositoryId"], "REPOID")
assert.Equal(t, inputs["title"], "hello")
assert.Equal(t, inputs["body"], "cash rules everything around me")
}),
)
output, err := runCommand(http, true, `-t hello -b "cash rules everything around me"`, nil)
if err != nil {
t.Errorf("error running command `issue create`: %v", err)
}
assert.Equal(t, "https://github.com/OWNER/REPO/issues/12\n", output.String())
}
func TestIssueCreate_recover(t *testing.T) {
http := &httpmock.Registry{}
defer http.Verify(t)
http.Register(
httpmock.GraphQL(`query IssueRepositoryInfo\b`),
httpmock.StringResponse(`
{ "data": { "repository": {
"id": "REPOID",
"hasIssuesEnabled": true
} } }`))
// Should only be one fetch of metadata.
http.Register(
httpmock.GraphQL(`query RepositoryLabelList\b`),
httpmock.StringResponse(`
{ "data": { "repository": { "labels": {
"nodes": [
{ "name": "TODO", "id": "TODOID" },
{ "name": "bug", "id": "BUGID" }
],
"pageInfo": { "hasNextPage": false }
} } } }
`))
http.Register(
httpmock.GraphQL(`mutation IssueCreate\b`),
httpmock.GraphQLMutation(`
{ "data": { "createIssue": { "issue": {
"URL": "https://github.com/OWNER/REPO/issues/12"
} } } }
`, func(inputs map[string]interface{}) {
assert.Equal(t, "recovered title", inputs["title"])
assert.Equal(t, "recovered body", inputs["body"])
assert.Equal(t, []interface{}{"BUGID", "TODOID"}, inputs["labelIds"])
}))
pm := &prompter.PrompterMock{}
pm.InputFunc = func(p, d string) (string, error) {
if p == "Title (required)" {
return d, nil
} else {
return "", prompter.NoSuchPromptErr(p)
}
}
pm.MarkdownEditorFunc = func(p, d string, ba bool) (string, error) {
if p == "Body" {
return d, nil
} else {
return "", prompter.NoSuchPromptErr(p)
}
}
pm.SelectFunc = func(p, _ string, opts []string) (int, error) {
if p == "What's next?" {
return prompter.IndexFor(opts, "Submit")
} else {
return -1, prompter.NoSuchPromptErr(p)
}
}
tmpfile, err := os.CreateTemp(t.TempDir(), "testrecover*")
assert.NoError(t, err)
defer tmpfile.Close()
state := prShared.IssueMetadataState{
Title: "recovered title",
Body: "recovered body",
Labels: []string{"bug", "TODO"},
}
data, err := json.Marshal(state)
assert.NoError(t, err)
_, err = tmpfile.Write(data)
assert.NoError(t, err)
args := fmt.Sprintf("--recover '%s'", tmpfile.Name())
output, err := runCommandWithRootDirOverridden(http, true, args, "", pm)
if err != nil {
t.Errorf("error running command `issue create`: %v", err)
}
assert.Equal(t, "https://github.com/OWNER/REPO/issues/12\n", output.String())
}
func TestIssueCreate_nonLegacyTemplate(t *testing.T) {
http := &httpmock.Registry{}
defer http.Verify(t)
http.Register(
httpmock.GraphQL(`query IssueRepositoryInfo\b`),
httpmock.StringResponse(`
{ "data": { "repository": {
"id": "REPOID",
"hasIssuesEnabled": true
} } }`),
)
http.Register(
httpmock.GraphQL(`query IssueTemplates\b`),
httpmock.StringResponse(`
{ "data": { "repository": { "issueTemplates": [
{ "name": "Bug report",
"body": "Does not work :((" },
{ "name": "Submit a request",
"body": "I have a suggestion for an enhancement" }
] } } }`),
)
http.Register(
httpmock.GraphQL(`mutation IssueCreate\b`),
httpmock.GraphQLMutation(`
{ "data": { "createIssue": { "issue": {
"URL": "https://github.com/OWNER/REPO/issues/12"
} } } }`,
func(inputs map[string]interface{}) {
assert.Equal(t, inputs["repositoryId"], "REPOID")
assert.Equal(t, inputs["title"], "hello")
assert.Equal(t, inputs["body"], "I have a suggestion for an enhancement")
}),
)
pm := &prompter.PrompterMock{}
pm.MarkdownEditorFunc = func(p, d string, ba bool) (string, error) {
if p == "Body" {
return d, nil
} else {
return "", prompter.NoSuchPromptErr(p)
}
}
pm.SelectFunc = func(p, _ string, opts []string) (int, error) {
switch p {
case "What's next?":
return prompter.IndexFor(opts, "Submit")
case "Choose a template":
return prompter.IndexFor(opts, "Submit a request")
default:
return -1, prompter.NoSuchPromptErr(p)
}
}
output, err := runCommandWithRootDirOverridden(http, true, `-t hello`, "./fixtures/repoWithNonLegacyIssueTemplates", pm)
if err != nil {
t.Errorf("error running command `issue create`: %v", err)
}
assert.Equal(t, "https://github.com/OWNER/REPO/issues/12\n", output.String())
assert.Equal(t, "", output.BrowsedURL)
}
func TestIssueCreate_continueInBrowser(t *testing.T) {
http := &httpmock.Registry{}
defer http.Verify(t)
http.Register(
httpmock.GraphQL(`query IssueRepositoryInfo\b`),
httpmock.StringResponse(`
{ "data": { "repository": {
"id": "REPOID",
"hasIssuesEnabled": true
} } }`),
)
pm := &prompter.PrompterMock{}
pm.InputFunc = func(p, d string) (string, error) {
if p == "Title (required)" {
return "hello", nil
} else {
return "", prompter.NoSuchPromptErr(p)
}
}
pm.SelectFunc = func(p, _ string, opts []string) (int, error) {
if p == "What's next?" {
return prompter.IndexFor(opts, "Continue in browser")
} else {
return -1, prompter.NoSuchPromptErr(p)
}
}
_, cmdTeardown := run.Stub()
defer cmdTeardown(t)
output, err := runCommand(http, true, `-b body`, pm)
if err != nil {
t.Errorf("error running command `issue create`: %v", err)
}
assert.Equal(t, "", output.String())
assert.Equal(t, heredoc.Doc(`
Creating issue in OWNER/REPO
Opening https://github.com/OWNER/REPO/issues/new in your browser.
`), output.Stderr())
assert.Equal(t, "https://github.com/OWNER/REPO/issues/new?body=body&title=hello", output.BrowsedURL)
}
func TestIssueCreate_metadata(t *testing.T) {
http := &httpmock.Registry{}
defer http.Verify(t)
http.StubIssueRepoInfoResponse("OWNER", "REPO")
http.Register(
httpmock.GraphQL(`query RepositoryLabelList\b`),
httpmock.StringResponse(`
{ "data": { "repository": { "labels": {
"nodes": [
{ "name": "TODO", "id": "TODOID" },
{ "name": "bug", "id": "BUGID" }
],
"pageInfo": { "hasNextPage": false }
} } } }
`))
http.Register(
httpmock.GraphQL(`query RepositoryMilestoneList\b`),
httpmock.StringResponse(`
{ "data": { "repository": { "milestones": {
"nodes": [
{ "title": "GA", "id": "GAID" },
{ "title": "Big One.oh", "id": "BIGONEID" }
],
"pageInfo": { "hasNextPage": false }
} } } }
`))
http.Register(
httpmock.GraphQL(`query RepositoryProjectList\b`),
httpmock.StringResponse(`
{ "data": { "repository": { "projects": {
"nodes": [
{ "name": "Cleanup", "id": "CLEANUPID" },
{ "name": "Roadmap", "id": "ROADMAPID" }
],
"pageInfo": { "hasNextPage": false }
} } } }
`))
http.Register(
httpmock.GraphQL(`query OrganizationProjectList\b`),
httpmock.StringResponse(`
{ "data": { "organization": null },
"errors": [{
"type": "NOT_FOUND",
"path": [ "organization" ],
"message": "Could not resolve to an Organization with the login of 'OWNER'."
}]
}
`))
http.Register(
httpmock.GraphQL(`query RepositoryProjectV2List\b`),
httpmock.StringResponse(`
{ "data": { "repository": { "projectsV2": {
"nodes": [],
"pageInfo": { "hasNextPage": false }
} } } }
`))
http.Register(
httpmock.GraphQL(`query OrganizationProjectV2List\b`),
httpmock.StringResponse(`
{ "data": { "organization": { "projectsV2": {
"nodes": [],
"pageInfo": { "hasNextPage": false }
} } } }
`))
http.Register(
httpmock.GraphQL(`query UserProjectV2List\b`),
httpmock.StringResponse(`
{ "data": { "viewer": { "projectsV2": {
"nodes": [],
"pageInfo": { "hasNextPage": false }
} } } }
`))
http.Register(
httpmock.GraphQL(`mutation IssueCreate\b`),
httpmock.GraphQLMutation(`
{ "data": { "createIssue": { "issue": {
"id": "NEWISSUEID",
"URL": "https://github.com/OWNER/REPO/issues/12"
} } } }
`, func(inputs map[string]interface{}) {
assert.Equal(t, "TITLE", inputs["title"])
assert.Equal(t, "BODY", inputs["body"])
if v, ok := inputs["assigneeIds"]; ok {
t.Errorf("did not expect assigneeIds: %v", v)
}
assert.Equal(t, []interface{}{"BUGID", "TODOID"}, inputs["labelIds"])
assert.Equal(t, []interface{}{"ROADMAPID"}, inputs["projectIds"])
assert.Equal(t, "BIGONEID", inputs["milestoneId"])
assert.NotContains(t, inputs, "userIds")
assert.NotContains(t, inputs, "teamIds")
assert.NotContains(t, inputs, "projectV2Ids")
}))
http.Register(
httpmock.GraphQL(`mutation ReplaceActorsForAssignable\b`),
httpmock.GraphQLMutation(`
{ "data": { "replaceActorsForAssignable": { "__typename": "" } } }
`, func(inputs map[string]interface{}) {
assert.Equal(t, "NEWISSUEID", inputs["assignableId"])
assert.Equal(t, []interface{}{"monalisa"}, inputs["actorLogins"])
}))
output, err := runCommand(http, true, `-t TITLE -b BODY -a monalisa -l bug -l todo -p roadmap -m 'big one.oh'`, nil)
if err != nil {
t.Errorf("error running command `issue create`: %v", err)
}
assert.Equal(t, "https://github.com/OWNER/REPO/issues/12\n", output.String())
}
func TestIssueCreate_disabledIssues(t *testing.T) {
http := &httpmock.Registry{}
defer http.Verify(t)
http.Register(
httpmock.GraphQL(`query IssueRepositoryInfo\b`),
httpmock.StringResponse(`
{ "data": { "repository": {
"id": "REPOID",
"hasIssuesEnabled": false
} } }`),
)
_, err := runCommand(http, true, `-t heres -b johnny`, nil)
if err == nil || err.Error() != "the 'OWNER/REPO' repository has disabled issues" {
t.Errorf("error running command `issue create`: %v", err)
}
}
func TestIssueCreate_AtMeAssignee(t *testing.T) {
http := &httpmock.Registry{}
defer http.Verify(t)
http.Register(
httpmock.GraphQL(`query UserCurrent\b`),
httpmock.StringResponse(`
{ "data": {
"viewer": { "login": "MonaLisa" }
} }
`),
)
http.Register(
httpmock.GraphQL(`query IssueRepositoryInfo\b`),
httpmock.StringResponse(`
{ "data": { "repository": {
"id": "REPOID",
"hasIssuesEnabled": true
} } }
`))
http.Register(
httpmock.GraphQL(`mutation IssueCreate\b`),
httpmock.GraphQLMutation(`
{ "data": { "createIssue": { "issue": {
"id": "NEWISSUEID",
"URL": "https://github.com/OWNER/REPO/issues/12"
} } } }
`, func(inputs map[string]interface{}) {
assert.Equal(t, "hello", inputs["title"])
assert.Equal(t, "cash rules everything around me", inputs["body"])
if v, ok := inputs["assigneeIds"]; ok {
t.Errorf("did not expect assigneeIds: %v", v)
}
}))
http.Register(
httpmock.GraphQL(`mutation ReplaceActorsForAssignable\b`),
httpmock.GraphQLMutation(`
{ "data": { "replaceActorsForAssignable": { "__typename": "" } } }
`, func(inputs map[string]interface{}) {
assert.Equal(t, "NEWISSUEID", inputs["assignableId"])
assert.Equal(t, []interface{}{"MonaLisa", "someoneelse"}, inputs["actorLogins"])
}))
output, err := runCommand(http, true, `-a @me -a someoneelse -t hello -b "cash rules everything around me"`, nil)
if err != nil {
t.Errorf("error running command `issue create`: %v", err)
}
assert.Equal(t, "https://github.com/OWNER/REPO/issues/12\n", output.String())
}
func TestIssueCreate_AtCopilotAssignee(t *testing.T) {
http := &httpmock.Registry{}
defer http.Verify(t)
http.Register(
httpmock.GraphQL(`query IssueRepositoryInfo\b`),
httpmock.StringResponse(`
{ "data": { "repository": {
"id": "REPOID",
"hasIssuesEnabled": true
} } }
`))
http.Register(
httpmock.GraphQL(`mutation IssueCreate\b`),
httpmock.GraphQLMutation(`
{ "data": { "createIssue": { "issue": {
"id": "NEWISSUEID",
"URL": "https://github.com/OWNER/REPO/issues/12"
} } } }
`, func(inputs map[string]interface{}) {
assert.Equal(t, "hello", inputs["title"])
assert.Equal(t, "cash rules everything around me", inputs["body"])
if v, ok := inputs["assigneeIds"]; ok {
t.Errorf("did not expect assigneeIds: %v", v)
}
}))
http.Register(
httpmock.GraphQL(`mutation ReplaceActorsForAssignable\b`),
httpmock.GraphQLMutation(`
{ "data": { "replaceActorsForAssignable": { "__typename": "" } } }
`, func(inputs map[string]interface{}) {
assert.Equal(t, "NEWISSUEID", inputs["assignableId"])
assert.Equal(t, []interface{}{"copilot-swe-agent[bot]"}, inputs["actorLogins"])
}))
output, err := runCommand(http, true, `-a @copilot -t hello -b "cash rules everything around me"`, nil)
if err != nil {
t.Errorf("error running command `issue create`: %v", err)
}
assert.Equal(t, "https://github.com/OWNER/REPO/issues/12\n", output.String())
}
func TestIssueCreate_projectsV2(t *testing.T) {
http := &httpmock.Registry{}
defer http.Verify(t)
http.StubIssueRepoInfoResponse("OWNER", "REPO")
http.Register(
httpmock.GraphQL(`query RepositoryProjectList\b`),
httpmock.StringResponse(`
{ "data": { "repository": { "projects": {
"nodes": [],
"pageInfo": { "hasNextPage": false }
} } } }
`))
http.Register(
httpmock.GraphQL(`query OrganizationProjectList\b`),
httpmock.StringResponse(`
{ "data": { "organization": { "projects": {
"nodes": [],
"pageInfo": { "hasNextPage": false }
} } } }
`))
http.Register(
httpmock.GraphQL(`query RepositoryProjectV2List\b`),
httpmock.StringResponse(`
{ "data": { "repository": { "projectsV2": {
"nodes": [
{ "title": "CleanupV2", "id": "CLEANUPV2ID" },
{ "title": "RoadmapV2", "id": "ROADMAPV2ID" }
],
"pageInfo": { "hasNextPage": false }
} } } }
`))
http.Register(
httpmock.GraphQL(`query OrganizationProjectV2List\b`),
httpmock.StringResponse(`
{ "data": { "organization": { "projectsV2": {
"nodes": [
{ "title": "TriageV2", "id": "TRIAGEV2ID" }
],
"pageInfo": { "hasNextPage": false }
} } } }
`))
http.Register(
httpmock.GraphQL(`query UserProjectV2List\b`),
httpmock.StringResponse(`
{ "data": { "viewer": { "projectsV2": {
"nodes": [
{ "title": "MonalisaV2", "id": "MONALISAV2ID" }
],
"pageInfo": { "hasNextPage": false }
} } } }
`))
http.Register(
httpmock.GraphQL(`mutation IssueCreate\b`),
httpmock.GraphQLMutation(`
{ "data": { "createIssue": { "issue": {
"id": "Issue#1",
"URL": "https://github.com/OWNER/REPO/issues/12"
} } } }
`, func(inputs map[string]interface{}) {
assert.Equal(t, "TITLE", inputs["title"])
assert.Equal(t, "BODY", inputs["body"])
assert.Nil(t, inputs["projectIds"])
assert.NotContains(t, inputs, "projectV2Ids")
}))
http.Register(
httpmock.GraphQL(`mutation UpdateProjectV2Items\b`),
httpmock.GraphQLQuery(`
{ "data": { "add_000": { "item": {
"id": "1"
} } } }
`, func(mutations string, inputs map[string]interface{}) {
variables, err := json.Marshal(inputs)
assert.NoError(t, err)
expectedMutations := "mutation UpdateProjectV2Items($input_000: AddProjectV2ItemByIdInput!) {add_000: addProjectV2ItemById(input: $input_000) { item { id } }}"
expectedVariables := `{"input_000":{"contentId":"Issue#1","projectId":"ROADMAPV2ID"}}`
assert.Equal(t, expectedMutations, mutations)
assert.Equal(t, expectedVariables, string(variables))
}))
output, err := runCommand(http, true, `-t TITLE -b BODY -p roadmapv2`, nil)
if err != nil {
t.Errorf("error running command `issue create`: %v", err)
}
assert.Equal(t, "https://github.com/OWNER/REPO/issues/12\n", output.String())
}
func Test_createRun_issuesV2(t *testing.T) {
tests := []struct {
name string
opts CreateOptions
httpStubs func(*testing.T, *httpmock.Registry)
wantsStdout string
wantsStderr string
wantsErr string
}{
{
name: "create with type",
opts: CreateOptions{
Detector: &fd.EnabledDetectorMock{},
Title: "bug title",
Body: "bug body",
IssueType: "Bug",
},
httpStubs: func(t *testing.T, r *httpmock.Registry) {
r.Register(
httpmock.GraphQL(`query IssueRepositoryInfo\b`),
httpmock.StringResponse(`
{ "data": { "repository": {
"id": "REPOID",
"hasIssuesEnabled": true
} } }`))
r.Register(
httpmock.GraphQL(`mutation IssueCreate\b`),
httpmock.StringResponse(`
{ "data": { "createIssue": { "issue": {
"id": "ISSUE_ID_123",
"URL": "https://github.com/OWNER/REPO/issues/123"
} } } }`))
r.Register(
httpmock.GraphQL(`query RepositoryIssueTypes\b`),
httpmock.StringResponse(`
{ "data": { "repository": { "issueTypes": { "nodes": [
{ "id": "IT_1", "name": "Bug", "description": "", "color": "d73a4a" },
{ "id": "IT_2", "name": "Feature", "description": "", "color": "0075ca" },
{ "id": "IT_3", "name": "Task", "description": "", "color": "e4e669" }
] } } } }`))
r.Register(
httpmock.GraphQL(`mutation UpdateIssueIssueType\b`),
httpmock.GraphQLMutation(`
{ "data": { "updateIssueIssueType": { "issue": { "id": "ISSUE_ID_123" } } } }`,
func(inputs map[string]interface{}) {
assert.Equal(t, "ISSUE_ID_123", inputs["issueId"])
assert.Equal(t, "IT_1", inputs["issueTypeId"])
}))
},
wantsStdout: "https://github.com/OWNER/REPO/issues/123\n",
wantsStderr: "\nCreating issue in OWNER/REPO\n\n",
},
{
name: "create with type not found",
opts: CreateOptions{
Detector: &fd.EnabledDetectorMock{},
Title: "bug title",
Body: "bug body",
IssueType: "Bugz",
},
httpStubs: func(t *testing.T, r *httpmock.Registry) {
r.Register(
httpmock.GraphQL(`query IssueRepositoryInfo\b`),
httpmock.StringResponse(`
{ "data": { "repository": {
"id": "REPOID",
"hasIssuesEnabled": true
} } }`))
r.Register(
httpmock.GraphQL(`mutation IssueCreate\b`),
httpmock.StringResponse(`
{ "data": { "createIssue": { "issue": {
"id": "ISSUE_ID_123",
"URL": "https://github.com/OWNER/REPO/issues/123"
} } } }`))
r.Register(
httpmock.GraphQL(`query RepositoryIssueTypes\b`),
httpmock.StringResponse(`
{ "data": { "repository": { "issueTypes": { "nodes": [
{ "id": "IT_1", "name": "Bug", "description": "", "color": "d73a4a" },
{ "id": "IT_2", "name": "Feature", "description": "", "color": "0075ca" },
{ "id": "IT_3", "name": "Task", "description": "", "color": "e4e669" }
] } } } }`))
},
wantsErr: `type "Bugz" not found; available types: Bug, Feature, Task`,
},
{
name: "create with parent",
opts: CreateOptions{
Detector: &fd.EnabledDetectorMock{},
Title: "child issue",
Body: "child body",
Parent: "100",
},
httpStubs: func(t *testing.T, r *httpmock.Registry) {
r.Register(
httpmock.GraphQL(`query IssueRepositoryInfo\b`),
httpmock.StringResponse(`
{ "data": { "repository": {
"id": "REPOID",
"hasIssuesEnabled": true
} } }`))
r.Register(
httpmock.GraphQL(`mutation IssueCreate\b`),
httpmock.StringResponse(`
{ "data": { "createIssue": { "issue": {
"id": "ISSUE_ID_123",
"URL": "https://github.com/OWNER/REPO/issues/123"
} } } }`))
r.Register(
httpmock.GraphQL(`query IssueNodeID\b`),
httpmock.StringResponse(`
{ "data": { "repository": { "issue": { "id": "PARENT_ID_100" } } } }`))
r.Register(
httpmock.GraphQL(`mutation AddSubIssue\b`),
httpmock.GraphQLMutation(`
{ "data": { "addSubIssue": { "issue": { "id": "PARENT_ID_100" } } } }`,
func(inputs map[string]interface{}) {
assert.Equal(t, "PARENT_ID_100", inputs["issueId"])
assert.Equal(t, "ISSUE_ID_123", inputs["subIssueId"])
assert.Equal(t, false, inputs["replaceParent"])
}))
},
wantsStdout: "https://github.com/OWNER/REPO/issues/123\n",
wantsStderr: "\nCreating issue in OWNER/REPO\n\n",
},
{
name: "create with blocked-by and blocking",
opts: CreateOptions{
Detector: &fd.EnabledDetectorMock{},
Title: "blocked issue",
Body: "blocked body",
BlockedBy: []string{"200"},
Blocking: []string{"300"},
},
httpStubs: func(t *testing.T, r *httpmock.Registry) {
r.Register(
httpmock.GraphQL(`query IssueRepositoryInfo\b`),
httpmock.StringResponse(`
{ "data": { "repository": {
"id": "REPOID",
"hasIssuesEnabled": true
} } }`))
r.Register(
httpmock.GraphQL(`mutation IssueCreate\b`),
httpmock.StringResponse(`
{ "data": { "createIssue": { "issue": {
"id": "ISSUE_ID_123",
"URL": "https://github.com/OWNER/REPO/issues/123"
} } } }`))
// IssueNodeID for --blocked-by 200
r.Register(
httpmock.GraphQL(`query IssueNodeID\b`),
httpmock.StringResponse(`
{ "data": { "repository": { "issue": { "id": "BLOCKER_ID_200" } } } }`))
// AddBlockedBy for --blocked-by: new issue is blocked by #200
r.Register(
httpmock.GraphQL(`mutation AddBlockedBy\b`),
httpmock.GraphQLMutation(`
{ "data": { "addBlockedBy": { "issue": { "id": "ISSUE_ID_123" } } } }`,
func(inputs map[string]interface{}) {
assert.Equal(t, "ISSUE_ID_123", inputs["issueId"])
assert.Equal(t, "BLOCKER_ID_200", inputs["blockingIssueId"])
}))
// IssueNodeID for --blocking 300
r.Register(
httpmock.GraphQL(`query IssueNodeID\b`),
httpmock.StringResponse(`
{ "data": { "repository": { "issue": { "id": "BLOCKED_ID_300" } } } }`))
// AddBlockedBy for --blocking: #300 is blocked by new issue (swapped args)
r.Register(
httpmock.GraphQL(`mutation AddBlockedBy\b`),
httpmock.GraphQLMutation(`
{ "data": { "addBlockedBy": { "issue": { "id": "BLOCKED_ID_300" } } } }`,
func(inputs map[string]interface{}) {
assert.Equal(t, "BLOCKED_ID_300", inputs["issueId"])
assert.Equal(t, "ISSUE_ID_123", inputs["blockingIssueId"])
}))
},
wantsStdout: "https://github.com/OWNER/REPO/issues/123\n",
wantsStderr: "\nCreating issue in OWNER/REPO\n\n",
},
{
name: "blocked-by unsupported on GHES",
opts: CreateOptions{
Detector: &fd.DisabledDetectorMock{},
Title: "blocked issue",
Body: "blocked body",
BlockedBy: []string{"200"},
},
httpStubs: func(t *testing.T, r *httpmock.Registry) {
r.Register(
httpmock.GraphQL(`query IssueRepositoryInfo\b`),
httpmock.StringResponse(`
{ "data": { "repository": {
"id": "REPOID",
"hasIssuesEnabled": true
} } }`))
r.Register(
httpmock.GraphQL(`mutation IssueCreate\b`),
httpmock.StringResponse(`
{ "data": { "createIssue": { "issue": {
"id": "ISSUE_ID_123",
"URL": "https://github.com/OWNER/REPO/issues/123"
} } } }`))
},
wantsErr: "issue relationships are not supported on this GitHub Enterprise Server version",
},
{
name: "blocking unsupported on GHES",
opts: CreateOptions{
Detector: &fd.DisabledDetectorMock{},
Title: "blocking issue",
Body: "blocking body",
Blocking: []string{"300"},
},
httpStubs: func(t *testing.T, r *httpmock.Registry) {
r.Register(
httpmock.GraphQL(`query IssueRepositoryInfo\b`),
httpmock.StringResponse(`
{ "data": { "repository": {
"id": "REPOID",
"hasIssuesEnabled": true
} } }`))
r.Register(
httpmock.GraphQL(`mutation IssueCreate\b`),
httpmock.StringResponse(`
{ "data": { "createIssue": { "issue": {
"id": "ISSUE_ID_123",
"URL": "https://github.com/OWNER/REPO/issues/123"
} } } }`))
},
wantsErr: "issue relationships are not supported on this GitHub Enterprise Server version",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
httpReg := &httpmock.Registry{}
defer httpReg.Verify(t)
if tt.httpStubs != nil {
tt.httpStubs(t, httpReg)
}
ios, _, stdout, stderr := iostreams.Test()
ios.SetStdoutTTY(true)
opts := &tt.opts
opts.IO = ios
opts.HttpClient = func() (*http.Client, error) {
return &http.Client{Transport: httpReg}, nil
}
opts.BaseRepo = func() (ghrepo.Interface, error) {
return ghrepo.New("OWNER", "REPO"), nil
}
opts.Browser = &browser.Stub{}
err := createRun(opts)
if tt.wantsErr == "" {
require.NoError(t, err)
} else {
assert.EqualError(t, err, tt.wantsErr)
return
}
assert.Equal(t, tt.wantsStdout, stdout.String())
assert.Equal(t, tt.wantsStderr, stderr.String())
})
}
}
// TODO projectsV1Deprecation
// Remove this test.
func TestProjectsV1Deprecation(t *testing.T) {
t.Run("non-interactive submission", func(t *testing.T) {
t.Run("when projects v1 is supported, queries for it", func(t *testing.T) {
ios, _, _, _ := iostreams.Test()
reg := &httpmock.Registry{}
reg.StubIssueRepoInfoResponse("OWNER", "REPO")
reg.Register(
// ( is required to avoid matching projectsV2
httpmock.GraphQL(`projects\(`),
// Simulate a GraphQL error to early exit the test.
httpmock.StatusStringResponse(500, ""),
)
_, cmdTeardown := run.Stub()
defer cmdTeardown(t)
// Ignore the error because we have no way to really stub it without
// fully stubbing a GQL error structure in the request body.
_ = createRun(&CreateOptions{
IO: ios,
HttpClient: func() (*http.Client, error) {
return &http.Client{Transport: reg}, nil
},
BaseRepo: func() (ghrepo.Interface, error) {
return ghrepo.New("OWNER", "REPO"), nil
},
Detector: &fd.EnabledDetectorMock{},
Title: "Test Title",
Body: "Test Body",
// Required to force a lookup of projects
Projects: []string{"Project"},
})
// Verify that our request contained projects
reg.Verify(t)
})
t.Run("when projects v1 is not supported, does not query for it", func(t *testing.T) {
ios, _, _, _ := iostreams.Test()
reg := &httpmock.Registry{}
reg.StubIssueRepoInfoResponse("OWNER", "REPO")
// ( is required to avoid matching projectsV2
reg.Exclude(t, httpmock.GraphQL(`projects\(`))
_, cmdTeardown := run.Stub()
defer cmdTeardown(t)
// Ignore the error because we're not really interested in it.
_ = createRun(&CreateOptions{
IO: ios,
HttpClient: func() (*http.Client, error) {
return &http.Client{Transport: reg}, nil
},
BaseRepo: func() (ghrepo.Interface, error) {
return ghrepo.New("OWNER", "REPO"), nil
},
Detector: &fd.DisabledDetectorMock{},
Title: "Test Title",
Body: "Test Body",
// Required to force a lookup of projects
Projects: []string{"Project"},
})
// Verify that our request contained projectCards
reg.Verify(t)
})
})
t.Run("web mode", func(t *testing.T) {
t.Run("when projects v1 is supported, queries for it", func(t *testing.T) {
ios, _, _, _ := iostreams.Test()
reg := &httpmock.Registry{}
reg.Register(
// ( is required to avoid matching projectsV2
httpmock.GraphQL(`projects\(`),
// Simulate a GraphQL error to early exit the test.
httpmock.StatusStringResponse(500, ""),
)
_, cmdTeardown := run.Stub()
defer cmdTeardown(t)
// Ignore the error because we have no way to really stub it without
// fully stubbing a GQL error structure in the request body.
_ = createRun(&CreateOptions{
IO: ios,
HttpClient: func() (*http.Client, error) {
return &http.Client{Transport: reg}, nil
},
BaseRepo: func() (ghrepo.Interface, error) {
return ghrepo.New("OWNER", "REPO"), nil
},
Detector: &fd.EnabledDetectorMock{},
WebMode: true,
// Required to force a lookup of projects
Projects: []string{"Project"},
})
// Verify that our request contained projects
reg.Verify(t)
})
t.Run("when projects v1 is not supported, does not query for it", func(t *testing.T) {
ios, _, _, _ := iostreams.Test()
reg := &httpmock.Registry{}
// ( is required to avoid matching projectsV2
reg.Exclude(t, httpmock.GraphQL(`projects\(`))
_, cmdTeardown := run.Stub()
defer cmdTeardown(t)
// Ignore the error because we're not really interested in it.
_ = createRun(&CreateOptions{
IO: ios,
HttpClient: func() (*http.Client, error) {
return &http.Client{Transport: reg}, nil
},
BaseRepo: func() (ghrepo.Interface, error) {
return ghrepo.New("OWNER", "REPO"), nil
},
Detector: &fd.DisabledDetectorMock{},
WebMode: true,
// Required to force a lookup of projects
Projects: []string{"Project"},
})
// Verify that our request contained projectCards
reg.Verify(t)
})
})
}