1package create
2
3import (
4	"bytes"
5	"encoding/json"
6	"fmt"
7	"io/ioutil"
8	"net/http"
9	"path/filepath"
10	"testing"
11
12	"github.com/MakeNowJust/heredoc"
13	"github.com/cli/cli/v2/api"
14	"github.com/cli/cli/v2/context"
15	"github.com/cli/cli/v2/git"
16	"github.com/cli/cli/v2/internal/config"
17	"github.com/cli/cli/v2/internal/ghrepo"
18	"github.com/cli/cli/v2/internal/run"
19	"github.com/cli/cli/v2/pkg/cmd/pr/shared"
20	prShared "github.com/cli/cli/v2/pkg/cmd/pr/shared"
21	"github.com/cli/cli/v2/pkg/cmdutil"
22	"github.com/cli/cli/v2/pkg/httpmock"
23	"github.com/cli/cli/v2/pkg/iostreams"
24	"github.com/cli/cli/v2/pkg/prompt"
25	"github.com/cli/cli/v2/test"
26	"github.com/google/shlex"
27	"github.com/stretchr/testify/assert"
28	"github.com/stretchr/testify/require"
29)
30
31func TestNewCmdCreate(t *testing.T) {
32	tmpFile := filepath.Join(t.TempDir(), "my-body.md")
33	err := ioutil.WriteFile(tmpFile, []byte("a body from file"), 0600)
34	require.NoError(t, err)
35
36	tests := []struct {
37		name      string
38		tty       bool
39		stdin     string
40		cli       string
41		wantsErr  bool
42		wantsOpts CreateOptions
43	}{
44		{
45			name:     "empty non-tty",
46			tty:      false,
47			cli:      "",
48			wantsErr: true,
49		},
50		{
51			name:     "empty tty",
52			tty:      true,
53			cli:      "",
54			wantsErr: false,
55			wantsOpts: CreateOptions{
56				Title:               "",
57				TitleProvided:       false,
58				Body:                "",
59				BodyProvided:        false,
60				Autofill:            false,
61				RecoverFile:         "",
62				WebMode:             false,
63				IsDraft:             false,
64				BaseBranch:          "",
65				HeadBranch:          "",
66				MaintainerCanModify: true,
67			},
68		},
69		{
70			name:     "body from stdin",
71			tty:      false,
72			stdin:    "this is on standard input",
73			cli:      "-t mytitle -F -",
74			wantsErr: false,
75			wantsOpts: CreateOptions{
76				Title:               "mytitle",
77				TitleProvided:       true,
78				Body:                "this is on standard input",
79				BodyProvided:        true,
80				Autofill:            false,
81				RecoverFile:         "",
82				WebMode:             false,
83				IsDraft:             false,
84				BaseBranch:          "",
85				HeadBranch:          "",
86				MaintainerCanModify: true,
87			},
88		},
89		{
90			name:     "body from file",
91			tty:      false,
92			cli:      fmt.Sprintf("-t mytitle -F '%s'", tmpFile),
93			wantsErr: false,
94			wantsOpts: CreateOptions{
95				Title:               "mytitle",
96				TitleProvided:       true,
97				Body:                "a body from file",
98				BodyProvided:        true,
99				Autofill:            false,
100				RecoverFile:         "",
101				WebMode:             false,
102				IsDraft:             false,
103				BaseBranch:          "",
104				HeadBranch:          "",
105				MaintainerCanModify: true,
106			},
107		},
108	}
109	for _, tt := range tests {
110		t.Run(tt.name, func(t *testing.T) {
111			io, stdin, stdout, stderr := iostreams.Test()
112			if tt.stdin != "" {
113				_, _ = stdin.WriteString(tt.stdin)
114			} else if tt.tty {
115				io.SetStdinTTY(true)
116				io.SetStdoutTTY(true)
117			}
118
119			f := &cmdutil.Factory{
120				IOStreams: io,
121			}
122
123			var opts *CreateOptions
124			cmd := NewCmdCreate(f, func(o *CreateOptions) error {
125				opts = o
126				return nil
127			})
128
129			args, err := shlex.Split(tt.cli)
130			require.NoError(t, err)
131			cmd.SetArgs(args)
132			_, err = cmd.ExecuteC()
133			if tt.wantsErr {
134				assert.Error(t, err)
135				return
136			} else {
137				require.NoError(t, err)
138			}
139
140			assert.Equal(t, "", stdout.String())
141			assert.Equal(t, "", stderr.String())
142
143			assert.Equal(t, tt.wantsOpts.Body, opts.Body)
144			assert.Equal(t, tt.wantsOpts.BodyProvided, opts.BodyProvided)
145			assert.Equal(t, tt.wantsOpts.Title, opts.Title)
146			assert.Equal(t, tt.wantsOpts.TitleProvided, opts.TitleProvided)
147			assert.Equal(t, tt.wantsOpts.Autofill, opts.Autofill)
148			assert.Equal(t, tt.wantsOpts.WebMode, opts.WebMode)
149			assert.Equal(t, tt.wantsOpts.RecoverFile, opts.RecoverFile)
150			assert.Equal(t, tt.wantsOpts.IsDraft, opts.IsDraft)
151			assert.Equal(t, tt.wantsOpts.MaintainerCanModify, opts.MaintainerCanModify)
152			assert.Equal(t, tt.wantsOpts.BaseBranch, opts.BaseBranch)
153			assert.Equal(t, tt.wantsOpts.HeadBranch, opts.HeadBranch)
154		})
155	}
156}
157
158func runCommand(rt http.RoundTripper, remotes context.Remotes, branch string, isTTY bool, cli string) (*test.CmdOut, error) {
159	return runCommandWithRootDirOverridden(rt, remotes, branch, isTTY, cli, "")
160}
161
162func runCommandWithRootDirOverridden(rt http.RoundTripper, remotes context.Remotes, branch string, isTTY bool, cli string, rootDir string) (*test.CmdOut, error) {
163	io, _, stdout, stderr := iostreams.Test()
164	io.SetStdoutTTY(isTTY)
165	io.SetStdinTTY(isTTY)
166	io.SetStderrTTY(isTTY)
167
168	browser := &cmdutil.TestBrowser{}
169	factory := &cmdutil.Factory{
170		IOStreams: io,
171		Browser:   browser,
172		HttpClient: func() (*http.Client, error) {
173			return &http.Client{Transport: rt}, nil
174		},
175		Config: func() (config.Config, error) {
176			return config.NewBlankConfig(), nil
177		},
178		Remotes: func() (context.Remotes, error) {
179			if remotes != nil {
180				return remotes, nil
181			}
182			return context.Remotes{
183				{
184					Remote: &git.Remote{
185						Name:     "origin",
186						Resolved: "base",
187					},
188					Repo: ghrepo.New("OWNER", "REPO"),
189				},
190			}, nil
191		},
192		Branch: func() (string, error) {
193			return branch, nil
194		},
195	}
196
197	cmd := NewCmdCreate(factory, func(opts *CreateOptions) error {
198		opts.RootDirOverride = rootDir
199		return createRun(opts)
200	})
201	cmd.PersistentFlags().StringP("repo", "R", "", "")
202
203	argv, err := shlex.Split(cli)
204	if err != nil {
205		return nil, err
206	}
207	cmd.SetArgs(argv)
208
209	cmd.SetIn(&bytes.Buffer{})
210	cmd.SetOut(ioutil.Discard)
211	cmd.SetErr(ioutil.Discard)
212
213	_, err = cmd.ExecuteC()
214	return &test.CmdOut{
215		OutBuf:     stdout,
216		ErrBuf:     stderr,
217		BrowsedURL: browser.BrowsedURL(),
218	}, err
219}
220
221func initFakeHTTP() *httpmock.Registry {
222	return &httpmock.Registry{}
223}
224
225func TestPRCreate_nontty_web(t *testing.T) {
226	http := initFakeHTTP()
227	defer http.Verify(t)
228
229	http.StubRepoInfoResponse("OWNER", "REPO", "master")
230
231	cs, cmdTeardown := run.Stub()
232	defer cmdTeardown(t)
233
234	cs.Register(`git status --porcelain`, 0, "")
235	cs.Register(`git( .+)? log( .+)? origin/master\.\.\.feature`, 0, "")
236
237	output, err := runCommand(http, nil, "feature", false, `--web --head=feature`)
238	require.NoError(t, err)
239
240	assert.Equal(t, "", output.String())
241	assert.Equal(t, "", output.Stderr())
242	assert.Equal(t, "https://github.com/OWNER/REPO/compare/master...feature?body=&expand=1", output.BrowsedURL)
243}
244
245func TestPRCreate_recover(t *testing.T) {
246	http := initFakeHTTP()
247	defer http.Verify(t)
248
249	http.StubRepoInfoResponse("OWNER", "REPO", "master")
250	shared.RunCommandFinder("feature", nil, nil)
251	http.Register(
252		httpmock.GraphQL(`query RepositoryResolveMetadataIDs\b`),
253		httpmock.StringResponse(`
254		{ "data": {
255			"u000": { "login": "jillValentine", "id": "JILLID" },
256			"repository": {},
257			"organization": {}
258		} }
259		`))
260	http.Register(
261		httpmock.GraphQL(`mutation PullRequestCreateRequestReviews\b`),
262		httpmock.GraphQLMutation(`
263		{ "data": { "requestReviews": {
264			"clientMutationId": ""
265		} } }
266	`, func(inputs map[string]interface{}) {
267			assert.Equal(t, []interface{}{"JILLID"}, inputs["userIds"])
268		}))
269	http.Register(
270		httpmock.GraphQL(`mutation PullRequestCreate\b`),
271		httpmock.GraphQLMutation(`
272		{ "data": { "createPullRequest": { "pullRequest": {
273			"URL": "https://github.com/OWNER/REPO/pull/12"
274		} } } }
275		`, func(input map[string]interface{}) {
276			assert.Equal(t, "recovered title", input["title"].(string))
277			assert.Equal(t, "recovered body", input["body"].(string))
278		}))
279
280	cs, cmdTeardown := run.Stub()
281	defer cmdTeardown(t)
282
283	cs.Register(`git status --porcelain`, 0, "")
284	cs.Register(`git( .+)? log( .+)? origin/master\.\.\.feature`, 0, "")
285
286	as, teardown := prompt.InitAskStubber()
287	defer teardown()
288	as.Stub([]*prompt.QuestionStub{
289		{
290			Name:    "Title",
291			Default: true,
292		},
293	})
294	as.Stub([]*prompt.QuestionStub{
295		{
296			Name:    "Body",
297			Default: true,
298		},
299	})
300	as.Stub([]*prompt.QuestionStub{
301		{
302			Name:  "confirmation",
303			Value: 0,
304		},
305	})
306
307	tmpfile, err := ioutil.TempFile(t.TempDir(), "testrecover*")
308	assert.NoError(t, err)
309	defer tmpfile.Close()
310
311	state := prShared.IssueMetadataState{
312		Title:     "recovered title",
313		Body:      "recovered body",
314		Reviewers: []string{"jillValentine"},
315	}
316
317	data, err := json.Marshal(state)
318	assert.NoError(t, err)
319
320	_, err = tmpfile.Write(data)
321	assert.NoError(t, err)
322
323	args := fmt.Sprintf("--recover '%s' -Hfeature", tmpfile.Name())
324
325	output, err := runCommandWithRootDirOverridden(http, nil, "feature", true, args, "")
326	assert.NoError(t, err)
327
328	assert.Equal(t, "https://github.com/OWNER/REPO/pull/12\n", output.String())
329}
330
331func TestPRCreate_nontty(t *testing.T) {
332	http := initFakeHTTP()
333	defer http.Verify(t)
334
335	http.StubRepoInfoResponse("OWNER", "REPO", "master")
336	shared.RunCommandFinder("feature", nil, nil)
337	http.Register(
338		httpmock.GraphQL(`mutation PullRequestCreate\b`),
339		httpmock.GraphQLMutation(`
340			{ "data": { "createPullRequest": { "pullRequest": {
341				"URL": "https://github.com/OWNER/REPO/pull/12"
342			} } } }`,
343			func(input map[string]interface{}) {
344				assert.Equal(t, "REPOID", input["repositoryId"])
345				assert.Equal(t, "my title", input["title"])
346				assert.Equal(t, "my body", input["body"])
347				assert.Equal(t, "master", input["baseRefName"])
348				assert.Equal(t, "feature", input["headRefName"])
349			}),
350	)
351
352	cs, cmdTeardown := run.Stub()
353	defer cmdTeardown(t)
354
355	cs.Register(`git status --porcelain`, 0, "")
356
357	output, err := runCommand(http, nil, "feature", false, `-t "my title" -b "my body" -H feature`)
358	require.NoError(t, err)
359
360	assert.Equal(t, "", output.Stderr())
361	assert.Equal(t, "https://github.com/OWNER/REPO/pull/12\n", output.String())
362}
363
364func TestPRCreate(t *testing.T) {
365	http := initFakeHTTP()
366	defer http.Verify(t)
367
368	http.StubRepoInfoResponse("OWNER", "REPO", "master")
369	http.StubRepoResponse("OWNER", "REPO")
370	http.Register(
371		httpmock.GraphQL(`query UserCurrent\b`),
372		httpmock.StringResponse(`{"data": {"viewer": {"login": "OWNER"} } }`))
373	shared.RunCommandFinder("feature", nil, nil)
374	http.Register(
375		httpmock.GraphQL(`mutation PullRequestCreate\b`),
376		httpmock.GraphQLMutation(`
377		{ "data": { "createPullRequest": { "pullRequest": {
378			"URL": "https://github.com/OWNER/REPO/pull/12"
379		} } } }
380		`, func(input map[string]interface{}) {
381			assert.Equal(t, "REPOID", input["repositoryId"].(string))
382			assert.Equal(t, "my title", input["title"].(string))
383			assert.Equal(t, "my body", input["body"].(string))
384			assert.Equal(t, "master", input["baseRefName"].(string))
385			assert.Equal(t, "feature", input["headRefName"].(string))
386		}))
387
388	cs, cmdTeardown := run.Stub()
389	defer cmdTeardown(t)
390
391	cs.Register(`git status --porcelain`, 0, "")
392	cs.Register(`git config --get-regexp.+branch\\\.feature\\\.`, 0, "")
393	cs.Register(`git show-ref --verify -- HEAD refs/remotes/origin/feature`, 0, "")
394	cs.Register(`git push --set-upstream origin HEAD:feature`, 0, "")
395
396	ask, cleanupAsk := prompt.InitAskStubber()
397	defer cleanupAsk()
398	ask.StubOne(0)
399
400	output, err := runCommand(http, nil, "feature", true, `-t "my title" -b "my body"`)
401	require.NoError(t, err)
402
403	assert.Equal(t, "https://github.com/OWNER/REPO/pull/12\n", output.String())
404	assert.Equal(t, "\nCreating pull request for feature into master in OWNER/REPO\n\n", output.Stderr())
405}
406
407func TestPRCreate_NoMaintainerModify(t *testing.T) {
408	// TODO update this copypasta
409	http := initFakeHTTP()
410	defer http.Verify(t)
411
412	http.StubRepoInfoResponse("OWNER", "REPO", "master")
413	http.StubRepoResponse("OWNER", "REPO")
414	http.Register(
415		httpmock.GraphQL(`query UserCurrent\b`),
416		httpmock.StringResponse(`{"data": {"viewer": {"login": "OWNER"} } }`))
417	shared.RunCommandFinder("feature", nil, nil)
418	http.Register(
419		httpmock.GraphQL(`mutation PullRequestCreate\b`),
420		httpmock.GraphQLMutation(`
421		{ "data": { "createPullRequest": { "pullRequest": {
422			"URL": "https://github.com/OWNER/REPO/pull/12"
423		} } } }
424		`, func(input map[string]interface{}) {
425			assert.Equal(t, false, input["maintainerCanModify"].(bool))
426			assert.Equal(t, "REPOID", input["repositoryId"].(string))
427			assert.Equal(t, "my title", input["title"].(string))
428			assert.Equal(t, "my body", input["body"].(string))
429			assert.Equal(t, "master", input["baseRefName"].(string))
430			assert.Equal(t, "feature", input["headRefName"].(string))
431		}))
432
433	cs, cmdTeardown := run.Stub()
434	defer cmdTeardown(t)
435
436	cs.Register(`git config --get-regexp.+branch\\\.feature\\\.`, 0, "")
437	cs.Register(`git status --porcelain`, 0, "")
438	cs.Register(`git show-ref --verify -- HEAD refs/remotes/origin/feature`, 0, "")
439	cs.Register(`git push --set-upstream origin HEAD:feature`, 0, "")
440
441	ask, cleanupAsk := prompt.InitAskStubber()
442	defer cleanupAsk()
443	ask.StubOne(0)
444
445	output, err := runCommand(http, nil, "feature", true, `-t "my title" -b "my body" --no-maintainer-edit`)
446	require.NoError(t, err)
447
448	assert.Equal(t, "https://github.com/OWNER/REPO/pull/12\n", output.String())
449	assert.Equal(t, "\nCreating pull request for feature into master in OWNER/REPO\n\n", output.Stderr())
450}
451
452func TestPRCreate_createFork(t *testing.T) {
453	http := initFakeHTTP()
454	defer http.Verify(t)
455
456	http.StubRepoInfoResponse("OWNER", "REPO", "master")
457	http.StubRepoResponse("OWNER", "REPO")
458	http.Register(
459		httpmock.GraphQL(`query UserCurrent\b`),
460		httpmock.StringResponse(`{"data": {"viewer": {"login": "monalisa"} } }`))
461	shared.RunCommandFinder("feature", nil, nil)
462	http.Register(
463		httpmock.REST("POST", "repos/OWNER/REPO/forks"),
464		httpmock.StatusStringResponse(201, `
465		{ "node_id": "NODEID",
466		  "name": "REPO",
467		  "owner": {"login": "monalisa"}
468		}
469		`))
470	http.Register(
471		httpmock.GraphQL(`mutation PullRequestCreate\b`),
472		httpmock.GraphQLMutation(`
473		{ "data": { "createPullRequest": { "pullRequest": {
474			"URL": "https://github.com/OWNER/REPO/pull/12"
475		} } } }
476		`, func(input map[string]interface{}) {
477			assert.Equal(t, "REPOID", input["repositoryId"].(string))
478			assert.Equal(t, "master", input["baseRefName"].(string))
479			assert.Equal(t, "monalisa:feature", input["headRefName"].(string))
480		}))
481
482	cs, cmdTeardown := run.Stub()
483	defer cmdTeardown(t)
484
485	cs.Register(`git config --get-regexp.+branch\\\.feature\\\.`, 0, "")
486	cs.Register(`git status --porcelain`, 0, "")
487	cs.Register(`git show-ref --verify -- HEAD refs/remotes/origin/feature`, 0, "")
488	cs.Register(`git remote add -f fork https://github.com/monalisa/REPO.git`, 0, "")
489	cs.Register(`git push --set-upstream fork HEAD:feature`, 0, "")
490
491	ask, cleanupAsk := prompt.InitAskStubber()
492	defer cleanupAsk()
493	ask.StubOne(1)
494
495	output, err := runCommand(http, nil, "feature", true, `-t title -b body`)
496	require.NoError(t, err)
497
498	assert.Equal(t, "https://github.com/OWNER/REPO/pull/12\n", output.String())
499}
500
501func TestPRCreate_pushedToNonBaseRepo(t *testing.T) {
502	remotes := context.Remotes{
503		{
504			Remote: &git.Remote{
505				Name:     "upstream",
506				Resolved: "base",
507			},
508			Repo: ghrepo.New("OWNER", "REPO"),
509		},
510		{
511			Remote: &git.Remote{
512				Name:     "origin",
513				Resolved: "base",
514			},
515			Repo: ghrepo.New("monalisa", "REPO"),
516		},
517	}
518
519	http := initFakeHTTP()
520	defer http.Verify(t)
521
522	http.StubRepoInfoResponse("OWNER", "REPO", "master")
523	shared.RunCommandFinder("feature", nil, nil)
524	http.Register(
525		httpmock.GraphQL(`mutation PullRequestCreate\b`),
526		httpmock.GraphQLMutation(`
527		{ "data": { "createPullRequest": { "pullRequest": {
528			"URL": "https://github.com/OWNER/REPO/pull/12"
529		} } } }
530		`, func(input map[string]interface{}) {
531			assert.Equal(t, "REPOID", input["repositoryId"].(string))
532			assert.Equal(t, "master", input["baseRefName"].(string))
533			assert.Equal(t, "monalisa:feature", input["headRefName"].(string))
534		}))
535
536	cs, cmdTeardown := run.Stub()
537	defer cmdTeardown(t)
538
539	cs.Register("git status", 0, "")
540	cs.Register(`git config --get-regexp \^branch\\\.feature\\\.`, 1, "") // determineTrackingBranch
541	cs.Register("git show-ref --verify", 0, heredoc.Doc(`
542		deadbeef HEAD
543		deadb00f refs/remotes/upstream/feature
544		deadbeef refs/remotes/origin/feature
545	`)) // determineTrackingBranch
546
547	_, cleanupAsk := prompt.InitAskStubber()
548	defer cleanupAsk()
549
550	output, err := runCommand(http, remotes, "feature", true, `-t title -b body`)
551	require.NoError(t, err)
552
553	assert.Equal(t, "\nCreating pull request for monalisa:feature into master in OWNER/REPO\n\n", output.Stderr())
554	assert.Equal(t, "https://github.com/OWNER/REPO/pull/12\n", output.String())
555}
556
557func TestPRCreate_pushedToDifferentBranchName(t *testing.T) {
558	http := initFakeHTTP()
559	defer http.Verify(t)
560
561	http.StubRepoInfoResponse("OWNER", "REPO", "master")
562	shared.RunCommandFinder("feature", nil, nil)
563	http.Register(
564		httpmock.GraphQL(`mutation PullRequestCreate\b`),
565		httpmock.GraphQLMutation(`
566		{ "data": { "createPullRequest": { "pullRequest": {
567			"URL": "https://github.com/OWNER/REPO/pull/12"
568		} } } }
569		`, func(input map[string]interface{}) {
570			assert.Equal(t, "REPOID", input["repositoryId"].(string))
571			assert.Equal(t, "master", input["baseRefName"].(string))
572			assert.Equal(t, "my-feat2", input["headRefName"].(string))
573		}))
574
575	cs, cmdTeardown := run.Stub()
576	defer cmdTeardown(t)
577
578	cs.Register("git status", 0, "")
579	cs.Register(`git config --get-regexp \^branch\\\.feature\\\.`, 0, heredoc.Doc(`
580		branch.feature.remote origin
581		branch.feature.merge refs/heads/my-feat2
582	`)) // determineTrackingBranch
583	cs.Register("git show-ref --verify", 0, heredoc.Doc(`
584		deadbeef HEAD
585		deadbeef refs/remotes/origin/my-feat2
586	`)) // determineTrackingBranch
587
588	_, cleanupAsk := prompt.InitAskStubber()
589	defer cleanupAsk()
590
591	output, err := runCommand(http, nil, "feature", true, `-t title -b body`)
592	require.NoError(t, err)
593
594	assert.Equal(t, "\nCreating pull request for my-feat2 into master in OWNER/REPO\n\n", output.Stderr())
595	assert.Equal(t, "https://github.com/OWNER/REPO/pull/12\n", output.String())
596}
597
598func TestPRCreate_nonLegacyTemplate(t *testing.T) {
599	http := initFakeHTTP()
600	defer http.Verify(t)
601
602	http.StubRepoInfoResponse("OWNER", "REPO", "master")
603	shared.RunCommandFinder("feature", nil, nil)
604	http.Register(
605		httpmock.GraphQL(`mutation PullRequestCreate\b`),
606		httpmock.GraphQLMutation(`
607		{ "data": { "createPullRequest": { "pullRequest": {
608			"URL": "https://github.com/OWNER/REPO/pull/12"
609		} } } }
610		`, func(input map[string]interface{}) {
611			assert.Equal(t, "my title", input["title"].(string))
612			assert.Equal(t, "- commit 1\n- commit 0\n\nFixes a bug and Closes an issue", input["body"].(string))
613		}))
614
615	cs, cmdTeardown := run.Stub()
616	defer cmdTeardown(t)
617
618	cs.Register(`git( .+)? log( .+)? origin/master\.\.\.feature`, 0, "1234567890,commit 0\n2345678901,commit 1")
619	cs.Register(`git status --porcelain`, 0, "")
620
621	as, teardown := prompt.InitAskStubber()
622	defer teardown()
623	as.StubOne(0) // template
624	as.Stub([]*prompt.QuestionStub{
625		{
626			Name:    "Body",
627			Default: true,
628		},
629	}) // body
630	as.Stub([]*prompt.QuestionStub{
631		{
632			Name:  "confirmation",
633			Value: 0,
634		},
635	}) // confirm
636
637	output, err := runCommandWithRootDirOverridden(http, nil, "feature", true, `-t "my title" -H feature`, "./fixtures/repoWithNonLegacyPRTemplates")
638	require.NoError(t, err)
639
640	assert.Equal(t, "https://github.com/OWNER/REPO/pull/12\n", output.String())
641}
642
643func TestPRCreate_metadata(t *testing.T) {
644	http := initFakeHTTP()
645	defer http.Verify(t)
646
647	http.StubRepoInfoResponse("OWNER", "REPO", "master")
648	shared.RunCommandFinder("feature", nil, nil)
649	http.Register(
650		httpmock.GraphQL(`query RepositoryResolveMetadataIDs\b`),
651		httpmock.StringResponse(`
652		{ "data": {
653			"u000": { "login": "MonaLisa", "id": "MONAID" },
654			"u001": { "login": "hubot", "id": "HUBOTID" },
655			"repository": {
656				"l000": { "name": "bug", "id": "BUGID" },
657				"l001": { "name": "TODO", "id": "TODOID" }
658			},
659			"organization": {
660				"t000": { "slug": "core", "id": "COREID" },
661				"t001": { "slug": "robots", "id": "ROBOTID" }
662			}
663		} }
664		`))
665	http.Register(
666		httpmock.GraphQL(`query RepositoryMilestoneList\b`),
667		httpmock.StringResponse(`
668		{ "data": { "repository": { "milestones": {
669			"nodes": [
670				{ "title": "GA", "id": "GAID" },
671				{ "title": "Big One.oh", "id": "BIGONEID" }
672			],
673			"pageInfo": { "hasNextPage": false }
674		} } } }
675		`))
676	http.Register(
677		httpmock.GraphQL(`query RepositoryProjectList\b`),
678		httpmock.StringResponse(`
679		{ "data": { "repository": { "projects": {
680			"nodes": [
681				{ "name": "Cleanup", "id": "CLEANUPID" },
682				{ "name": "Roadmap", "id": "ROADMAPID" }
683			],
684			"pageInfo": { "hasNextPage": false }
685		} } } }
686		`))
687	http.Register(
688		httpmock.GraphQL(`query OrganizationProjectList\b`),
689		httpmock.StringResponse(`
690		{ "data": { "organization": { "projects": {
691			"nodes": [],
692			"pageInfo": { "hasNextPage": false }
693		} } } }
694		`))
695	http.Register(
696		httpmock.GraphQL(`mutation PullRequestCreate\b`),
697		httpmock.GraphQLMutation(`
698		{ "data": { "createPullRequest": { "pullRequest": {
699			"id": "NEWPULLID",
700			"URL": "https://github.com/OWNER/REPO/pull/12"
701		} } } }
702	`, func(inputs map[string]interface{}) {
703			assert.Equal(t, "TITLE", inputs["title"])
704			assert.Equal(t, "BODY", inputs["body"])
705			if v, ok := inputs["assigneeIds"]; ok {
706				t.Errorf("did not expect assigneeIds: %v", v)
707			}
708			if v, ok := inputs["userIds"]; ok {
709				t.Errorf("did not expect userIds: %v", v)
710			}
711		}))
712	http.Register(
713		httpmock.GraphQL(`mutation PullRequestCreateMetadata\b`),
714		httpmock.GraphQLMutation(`
715		{ "data": { "updatePullRequest": {
716			"clientMutationId": ""
717		} } }
718	`, func(inputs map[string]interface{}) {
719			assert.Equal(t, "NEWPULLID", inputs["pullRequestId"])
720			assert.Equal(t, []interface{}{"MONAID"}, inputs["assigneeIds"])
721			assert.Equal(t, []interface{}{"BUGID", "TODOID"}, inputs["labelIds"])
722			assert.Equal(t, []interface{}{"ROADMAPID"}, inputs["projectIds"])
723			assert.Equal(t, "BIGONEID", inputs["milestoneId"])
724		}))
725	http.Register(
726		httpmock.GraphQL(`mutation PullRequestCreateRequestReviews\b`),
727		httpmock.GraphQLMutation(`
728		{ "data": { "requestReviews": {
729			"clientMutationId": ""
730		} } }
731	`, func(inputs map[string]interface{}) {
732			assert.Equal(t, "NEWPULLID", inputs["pullRequestId"])
733			assert.Equal(t, []interface{}{"HUBOTID", "MONAID"}, inputs["userIds"])
734			assert.Equal(t, []interface{}{"COREID", "ROBOTID"}, inputs["teamIds"])
735			assert.Equal(t, true, inputs["union"])
736		}))
737
738	output, err := runCommand(http, nil, "feature", true, `-t TITLE -b BODY -H feature -a monalisa -l bug -l todo -p roadmap -m 'big one.oh' -r hubot -r monalisa -r /core -r /robots`)
739	assert.NoError(t, err)
740
741	assert.Equal(t, "https://github.com/OWNER/REPO/pull/12\n", output.String())
742}
743
744func TestPRCreate_alreadyExists(t *testing.T) {
745	http := initFakeHTTP()
746	defer http.Verify(t)
747
748	http.StubRepoInfoResponse("OWNER", "REPO", "master")
749	shared.RunCommandFinder("feature", &api.PullRequest{URL: "https://github.com/OWNER/REPO/pull/123"}, ghrepo.New("OWNER", "REPO"))
750
751	_, err := runCommand(http, nil, "feature", true, `-t title -b body -H feature`)
752	assert.EqualError(t, err, "a pull request for branch \"feature\" into branch \"master\" already exists:\nhttps://github.com/OWNER/REPO/pull/123")
753}
754
755func TestPRCreate_web(t *testing.T) {
756	http := initFakeHTTP()
757	defer http.Verify(t)
758
759	http.StubRepoInfoResponse("OWNER", "REPO", "master")
760	http.StubRepoResponse("OWNER", "REPO")
761	http.Register(
762		httpmock.GraphQL(`query UserCurrent\b`),
763		httpmock.StringResponse(`{"data": {"viewer": {"login": "OWNER"} } }`))
764
765	cs, cmdTeardown := run.Stub()
766	defer cmdTeardown(t)
767
768	cs.Register(`git config --get-regexp.+branch\\\.feature\\\.`, 0, "")
769	cs.Register(`git status --porcelain`, 0, "")
770	cs.Register(`git show-ref --verify -- HEAD refs/remotes/origin/feature`, 0, "")
771	cs.Register(`git( .+)? log( .+)? origin/master\.\.\.feature`, 0, "")
772	cs.Register(`git push --set-upstream origin HEAD:feature`, 0, "")
773
774	ask, cleanupAsk := prompt.InitAskStubber()
775	defer cleanupAsk()
776	ask.StubOne(0)
777
778	output, err := runCommand(http, nil, "feature", true, `--web`)
779	require.NoError(t, err)
780
781	assert.Equal(t, "", output.String())
782	assert.Equal(t, "Opening github.com/OWNER/REPO/compare/master...feature in your browser.\n", output.Stderr())
783	assert.Equal(t, "https://github.com/OWNER/REPO/compare/master...feature?body=&expand=1", output.BrowsedURL)
784}
785
786func TestPRCreate_webLongURL(t *testing.T) {
787	longBodyFile := filepath.Join(t.TempDir(), "long-body.txt")
788	err := ioutil.WriteFile(longBodyFile, make([]byte, 9216), 0600)
789	require.NoError(t, err)
790
791	http := initFakeHTTP()
792	defer http.Verify(t)
793
794	http.StubRepoInfoResponse("OWNER", "REPO", "master")
795
796	cs, cmdTeardown := run.Stub()
797	defer cmdTeardown(t)
798
799	cs.Register(`git status --porcelain`, 0, "")
800	cs.Register(`git( .+)? log( .+)? origin/master\.\.\.feature`, 0, "")
801
802	_, err = runCommand(http, nil, "feature", false, fmt.Sprintf("--body-file '%s' --web --head=feature", longBodyFile))
803	require.EqualError(t, err, "cannot open in browser: maximum URL length exceeded")
804}
805
806func TestPRCreate_webProject(t *testing.T) {
807	http := initFakeHTTP()
808	defer http.Verify(t)
809
810	http.StubRepoInfoResponse("OWNER", "REPO", "master")
811	http.StubRepoResponse("OWNER", "REPO")
812	http.Register(
813		httpmock.GraphQL(`query UserCurrent\b`),
814		httpmock.StringResponse(`{"data": {"viewer": {"login": "OWNER"} } }`))
815	http.Register(
816		httpmock.GraphQL(`query RepositoryProjectList\b`),
817		httpmock.StringResponse(`
818			{ "data": { "repository": { "projects": {
819				"nodes": [
820					{ "name": "Cleanup", "id": "CLEANUPID", "resourcePath": "/OWNER/REPO/projects/1" }
821				],
822				"pageInfo": { "hasNextPage": false }
823			} } } }
824			`))
825	http.Register(
826		httpmock.GraphQL(`query OrganizationProjectList\b`),
827		httpmock.StringResponse(`
828			{ "data": { "organization": { "projects": {
829				"nodes": [
830					{ "name": "Triage", "id": "TRIAGEID", "resourcePath": "/orgs/ORG/projects/1"  }
831				],
832				"pageInfo": { "hasNextPage": false }
833			} } } }
834			`))
835
836	cs, cmdTeardown := run.Stub()
837	defer cmdTeardown(t)
838
839	cs.Register(`git config --get-regexp.+branch\\\.feature\\\.`, 0, "")
840	cs.Register(`git status --porcelain`, 0, "")
841	cs.Register(`git show-ref --verify -- HEAD refs/remotes/origin/feature`, 0, "")
842	cs.Register(`git( .+)? log( .+)? origin/master\.\.\.feature`, 0, "")
843	cs.Register(`git push --set-upstream origin HEAD:feature`, 0, "")
844
845	ask, cleanupAsk := prompt.InitAskStubber()
846	defer cleanupAsk()
847	ask.StubOne(0)
848
849	output, err := runCommand(http, nil, "feature", true, `--web -p Triage`)
850	require.NoError(t, err)
851
852	assert.Equal(t, "", output.String())
853	assert.Equal(t, "Opening github.com/OWNER/REPO/compare/master...feature in your browser.\n", output.Stderr())
854	assert.Equal(t, "https://github.com/OWNER/REPO/compare/master...feature?body=&expand=1&projects=ORG%2F1", output.BrowsedURL)
855}
856
857func Test_determineTrackingBranch_empty(t *testing.T) {
858	cs, cmdTeardown := run.Stub()
859	defer cmdTeardown(t)
860
861	cs.Register(`git config --get-regexp.+branch\\\.feature\\\.`, 0, "")
862	cs.Register(`git show-ref --verify -- HEAD`, 0, "abc HEAD")
863
864	remotes := context.Remotes{}
865
866	ref := determineTrackingBranch(remotes, "feature")
867	if ref != nil {
868		t.Errorf("expected nil result, got %v", ref)
869	}
870}
871
872func Test_determineTrackingBranch_noMatch(t *testing.T) {
873	cs, cmdTeardown := run.Stub()
874	defer cmdTeardown(t)
875
876	cs.Register(`git config --get-regexp.+branch\\\.feature\\\.`, 0, "")
877	cs.Register("git show-ref --verify -- HEAD refs/remotes/origin/feature refs/remotes/upstream/feature", 0, "abc HEAD\nbca refs/remotes/origin/feature")
878
879	remotes := context.Remotes{
880		&context.Remote{
881			Remote: &git.Remote{Name: "origin"},
882			Repo:   ghrepo.New("hubot", "Spoon-Knife"),
883		},
884		&context.Remote{
885			Remote: &git.Remote{Name: "upstream"},
886			Repo:   ghrepo.New("octocat", "Spoon-Knife"),
887		},
888	}
889
890	ref := determineTrackingBranch(remotes, "feature")
891	if ref != nil {
892		t.Errorf("expected nil result, got %v", ref)
893	}
894}
895
896func Test_determineTrackingBranch_hasMatch(t *testing.T) {
897	cs, cmdTeardown := run.Stub()
898	defer cmdTeardown(t)
899
900	cs.Register(`git config --get-regexp.+branch\\\.feature\\\.`, 0, "")
901	cs.Register(`git show-ref --verify -- HEAD refs/remotes/origin/feature refs/remotes/upstream/feature$`, 0, heredoc.Doc(`
902		deadbeef HEAD
903		deadb00f refs/remotes/origin/feature
904		deadbeef refs/remotes/upstream/feature
905	`))
906
907	remotes := context.Remotes{
908		&context.Remote{
909			Remote: &git.Remote{Name: "origin"},
910			Repo:   ghrepo.New("hubot", "Spoon-Knife"),
911		},
912		&context.Remote{
913			Remote: &git.Remote{Name: "upstream"},
914			Repo:   ghrepo.New("octocat", "Spoon-Knife"),
915		},
916	}
917
918	ref := determineTrackingBranch(remotes, "feature")
919	if ref == nil {
920		t.Fatal("expected result, got nil")
921	}
922
923	assert.Equal(t, "upstream", ref.RemoteName)
924	assert.Equal(t, "feature", ref.BranchName)
925}
926
927func Test_determineTrackingBranch_respectTrackingConfig(t *testing.T) {
928	cs, cmdTeardown := run.Stub()
929	defer cmdTeardown(t)
930
931	cs.Register(`git config --get-regexp.+branch\\\.feature\\\.`, 0, heredoc.Doc(`
932		branch.feature.remote origin
933		branch.feature.merge refs/heads/great-feat
934	`))
935	cs.Register(`git show-ref --verify -- HEAD refs/remotes/origin/great-feat refs/remotes/origin/feature$`, 0, heredoc.Doc(`
936		deadbeef HEAD
937		deadb00f refs/remotes/origin/feature
938	`))
939
940	remotes := context.Remotes{
941		&context.Remote{
942			Remote: &git.Remote{Name: "origin"},
943			Repo:   ghrepo.New("hubot", "Spoon-Knife"),
944		},
945	}
946
947	ref := determineTrackingBranch(remotes, "feature")
948	if ref != nil {
949		t.Errorf("expected nil result, got %v", ref)
950	}
951}
952
953func Test_generateCompareURL(t *testing.T) {
954	tests := []struct {
955		name    string
956		ctx     CreateContext
957		state   prShared.IssueMetadataState
958		want    string
959		wantErr bool
960	}{
961		{
962			name: "basic",
963			ctx: CreateContext{
964				BaseRepo:        api.InitRepoHostname(&api.Repository{Name: "REPO", Owner: api.RepositoryOwner{Login: "OWNER"}}, "github.com"),
965				BaseBranch:      "main",
966				HeadBranchLabel: "feature",
967			},
968			want:    "https://github.com/OWNER/REPO/compare/main...feature?body=&expand=1",
969			wantErr: false,
970		},
971		{
972			name: "with labels",
973			ctx: CreateContext{
974				BaseRepo:        api.InitRepoHostname(&api.Repository{Name: "REPO", Owner: api.RepositoryOwner{Login: "OWNER"}}, "github.com"),
975				BaseBranch:      "a",
976				HeadBranchLabel: "b",
977			},
978			state: prShared.IssueMetadataState{
979				Labels: []string{"one", "two three"},
980			},
981			want:    "https://github.com/OWNER/REPO/compare/a...b?body=&expand=1&labels=one%2Ctwo+three",
982			wantErr: false,
983		},
984		{
985			name: "complex branch names",
986			ctx: CreateContext{
987				BaseRepo:        api.InitRepoHostname(&api.Repository{Name: "REPO", Owner: api.RepositoryOwner{Login: "OWNER"}}, "github.com"),
988				BaseBranch:      "main/trunk",
989				HeadBranchLabel: "owner:feature",
990			},
991			want:    "https://github.com/OWNER/REPO/compare/main%2Ftrunk...owner%3Afeature?body=&expand=1",
992			wantErr: false,
993		},
994	}
995	for _, tt := range tests {
996		t.Run(tt.name, func(t *testing.T) {
997			got, err := generateCompareURL(tt.ctx, tt.state)
998			if (err != nil) != tt.wantErr {
999				t.Errorf("generateCompareURL() error = %v, wantErr %v", err, tt.wantErr)
1000				return
1001			}
1002			if got != tt.want {
1003				t.Errorf("generateCompareURL() = %v, want %v", got, tt.want)
1004			}
1005		})
1006	}
1007}
1008