1396 lines
40 KiB
Go
1396 lines
40 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,
|
|
},
|
|
}
|
|
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)
|
|
})
|
|
}
|
|
}
|
|
|
|
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 RepositoryInfo\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 RepositoryInfo\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")
|
|
case "Assignees":
|
|
return prompter.IndexesFor(options, "Copilot (AI)", "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 RepositoryInfo\b`),
|
|
httpmock.StringResponse(`
|
|
{ "data": { "repository": {
|
|
"id": "REPOID",
|
|
"hasIssuesEnabled": true,
|
|
"viewerPermission": "WRITE"
|
|
} } }
|
|
`))
|
|
r.Register(
|
|
httpmock.GraphQL(`query RepositoryAssignableActors\b`),
|
|
httpmock.StringResponse(`
|
|
{ "data": { "repository": { "suggestedActors": {
|
|
"nodes": [
|
|
{ "login": "copilot-swe-agent", "id": "COPILOTID", "name": "Copilot (AI)", "__typename": "Bot" },
|
|
{ "login": "MonaLisa", "id": "MONAID", "name": "Mona Display Name", "__typename": "User" }
|
|
],
|
|
"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{}{"COPILOTID", "MONAID"}, inputs["assigneeIds"])
|
|
}))
|
|
},
|
|
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 RepositoryInfo\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 RepositoryInfo\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 RepositoryInfo\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 RepositoryInfo\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 RepositoryInfo\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.StubRepoInfoResponse("OWNER", "REPO", "main")
|
|
http.Register(
|
|
httpmock.GraphQL(`query RepositoryAssignableActors\b`),
|
|
httpmock.StringResponse(`
|
|
{ "data": { "repository": { "suggestedActors": {
|
|
"nodes": [
|
|
{ "login": "MonaLisa", "id": "MONAID", "name": "Mona Display Name", "__typename": "User" }
|
|
],
|
|
"pageInfo": { "hasNextPage": false }
|
|
} } } }
|
|
`))
|
|
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": {
|
|
"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.Equal(t, []interface{}{"MONAID"}, inputs["assigneeIds"])
|
|
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")
|
|
}))
|
|
|
|
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 RepositoryInfo\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 RepositoryInfo\b`),
|
|
httpmock.StringResponse(`
|
|
{ "data": { "repository": {
|
|
"id": "REPOID",
|
|
"hasIssuesEnabled": true
|
|
} } }
|
|
`))
|
|
http.Register(
|
|
httpmock.GraphQL(`query RepositoryAssignableActors\b`),
|
|
httpmock.StringResponse(`
|
|
{ "data": { "repository": { "suggestedActors": {
|
|
"nodes": [
|
|
{ "login": "MonaLisa", "id": "MONAID", "name": "Mona Display Name", "__typename": "User" },
|
|
{ "login": "SomeOneElse", "id": "SOMEID", "name": "Someone else", "__typename": "User" }
|
|
],
|
|
"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, "hello", inputs["title"])
|
|
assert.Equal(t, "cash rules everything around me", inputs["body"])
|
|
assert.Equal(t, []interface{}{"MONAID", "SOMEID"}, inputs["assigneeIds"])
|
|
}))
|
|
|
|
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 RepositoryInfo\b`),
|
|
httpmock.StringResponse(`
|
|
{ "data": { "repository": {
|
|
"id": "REPOID",
|
|
"hasIssuesEnabled": true
|
|
} } }
|
|
`))
|
|
http.Register(
|
|
httpmock.GraphQL(`query RepositoryAssignableActors\b`),
|
|
httpmock.StringResponse(`
|
|
{ "data": { "repository": { "suggestedActors": {
|
|
"nodes": [
|
|
{ "login": "copilot-swe-agent", "id": "COPILOTID", "name": "Copilot (AI)", "__typename": "Bot" }
|
|
],
|
|
"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, "hello", inputs["title"])
|
|
assert.Equal(t, "cash rules everything around me", inputs["body"])
|
|
assert.Equal(t, []interface{}{"COPILOTID"}, inputs["assigneeIds"])
|
|
}))
|
|
|
|
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.StubRepoInfoResponse("OWNER", "REPO", "main")
|
|
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())
|
|
}
|
|
|
|
// 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.StubRepoInfoResponse("OWNER", "REPO", "main")
|
|
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.StubRepoInfoResponse("OWNER", "REPO", "main")
|
|
// ( 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)
|
|
})
|
|
})
|
|
}
|