package create import ( "encoding/json" "errors" "fmt" "net/http" "os" "path/filepath" "strings" "testing" "github.com/MakeNowJust/heredoc" "github.com/cli/cli/v2/api" "github.com/cli/cli/v2/context" "github.com/cli/cli/v2/git" "github.com/cli/cli/v2/internal/browser" "github.com/cli/cli/v2/internal/config" "github.com/cli/cli/v2/internal/ghrepo" "github.com/cli/cli/v2/internal/prompter" "github.com/cli/cli/v2/internal/run" "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 wantsErr bool wantsOpts CreateOptions }{ { name: "empty non-tty", tty: false, cli: "", wantsErr: true, }, { name: "only title non-tty", tty: false, cli: "--title mytitle", wantsErr: true, }, { name: "minimum non-tty", tty: false, cli: "--title mytitle --body ''", wantsErr: false, wantsOpts: CreateOptions{ Title: "mytitle", TitleProvided: true, Body: "", BodyProvided: true, Autofill: false, RecoverFile: "", WebMode: false, IsDraft: false, BaseBranch: "", HeadBranch: "", MaintainerCanModify: true, }, }, { name: "empty tty", tty: true, cli: "", wantsErr: false, wantsOpts: CreateOptions{ Title: "", TitleProvided: false, Body: "", BodyProvided: false, Autofill: false, RecoverFile: "", WebMode: false, IsDraft: false, BaseBranch: "", HeadBranch: "", MaintainerCanModify: true, }, }, { name: "body from stdin", tty: false, stdin: "this is on standard input", cli: "-t mytitle -F -", wantsErr: false, wantsOpts: CreateOptions{ Title: "mytitle", TitleProvided: true, Body: "this is on standard input", BodyProvided: true, Autofill: false, RecoverFile: "", WebMode: false, IsDraft: false, BaseBranch: "", HeadBranch: "", MaintainerCanModify: true, }, }, { name: "body from file", tty: false, cli: fmt.Sprintf("-t mytitle -F '%s'", tmpFile), wantsErr: false, wantsOpts: CreateOptions{ Title: "mytitle", TitleProvided: true, Body: "a body from file", BodyProvided: true, Autofill: false, RecoverFile: "", WebMode: false, IsDraft: false, BaseBranch: "", HeadBranch: "", MaintainerCanModify: true, }, }, { name: "template from file name tty", tty: true, cli: "-t mytitle --template bug_fix.md", wantsErr: false, wantsOpts: CreateOptions{ Title: "mytitle", TitleProvided: true, Body: "", BodyProvided: false, Autofill: false, RecoverFile: "", WebMode: false, IsDraft: false, BaseBranch: "", HeadBranch: "", MaintainerCanModify: true, Template: "bug_fix.md", }, }, { name: "template from file name non-tty", tty: false, cli: "-t mytitle --template bug_fix.md", wantsErr: true, }, { name: "template and body", tty: false, cli: `-t mytitle --template bug_fix.md --body "pr body"`, wantsErr: true, }, { name: "template and body file", tty: false, cli: "-t mytitle --template bug_fix.md --body-file body_file.md", wantsErr: true, }, { name: "fill-first", tty: false, cli: "--fill-first", wantsErr: false, wantsOpts: CreateOptions{ Title: "", TitleProvided: false, Body: "", BodyProvided: false, Autofill: false, FillFirst: true, RecoverFile: "", WebMode: false, IsDraft: false, BaseBranch: "", HeadBranch: "", MaintainerCanModify: true, }, }, { name: "fill and fill-first", tty: false, cli: "--fill --fill-first", wantsErr: true, }, { name: "dry-run and web", tty: false, cli: "--web --dry-run", 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, } 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(stderr) cmd.SetErr(stderr) _, 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.BodyProvided, opts.BodyProvided) assert.Equal(t, tt.wantsOpts.Title, opts.Title) assert.Equal(t, tt.wantsOpts.TitleProvided, opts.TitleProvided) assert.Equal(t, tt.wantsOpts.Autofill, opts.Autofill) assert.Equal(t, tt.wantsOpts.FillFirst, opts.FillFirst) assert.Equal(t, tt.wantsOpts.WebMode, opts.WebMode) assert.Equal(t, tt.wantsOpts.RecoverFile, opts.RecoverFile) assert.Equal(t, tt.wantsOpts.IsDraft, opts.IsDraft) assert.Equal(t, tt.wantsOpts.MaintainerCanModify, opts.MaintainerCanModify) assert.Equal(t, tt.wantsOpts.BaseBranch, opts.BaseBranch) assert.Equal(t, tt.wantsOpts.HeadBranch, opts.HeadBranch) assert.Equal(t, tt.wantsOpts.Template, opts.Template) }) } } func Test_createRun(t *testing.T) { tests := []struct { name string setup func(*CreateOptions, *testing.T) func() cmdStubs func(*run.CommandStubber) promptStubs func(*prompter.PrompterMock) httpStubs func(*httpmock.Registry, *testing.T) expectedOutputs []string expectedOut string expectedErrOut string expectedBrowse string wantErr string tty bool }{ { name: "nontty web", setup: func(opts *CreateOptions, t *testing.T) func() { opts.WebMode = true opts.HeadBranch = "feature" return func() {} }, cmdStubs: func(cs *run.CommandStubber) { cs.Register(`git( .+)? log( .+)? origin/master\.\.\.feature`, 0, "") }, expectedBrowse: "https://github.com/OWNER/REPO/compare/master...feature?body=&expand=1", }, { name: "nontty", httpStubs: func(reg *httpmock.Registry, t *testing.T) { reg.Register( httpmock.GraphQL(`mutation PullRequestCreate\b`), httpmock.GraphQLMutation(` { "data": { "createPullRequest": { "pullRequest": { "URL": "https://github.com/OWNER/REPO/pull/12" } } } }`, func(input map[string]interface{}) { assert.Equal(t, "REPOID", input["repositoryId"]) assert.Equal(t, "my title", input["title"]) assert.Equal(t, "my body", input["body"]) assert.Equal(t, "master", input["baseRefName"]) assert.Equal(t, "feature", input["headRefName"]) })) }, setup: func(opts *CreateOptions, t *testing.T) func() { opts.TitleProvided = true opts.BodyProvided = true opts.Title = "my title" opts.Body = "my body" opts.HeadBranch = "feature" return func() {} }, expectedOut: "https://github.com/OWNER/REPO/pull/12\n", }, { name: "dry-run-nontty-with-default-base", tty: false, setup: func(opts *CreateOptions, t *testing.T) func() { opts.TitleProvided = true opts.BodyProvided = true opts.Title = "my title" opts.Body = "my body" opts.HeadBranch = "feature" opts.DryRun = true return func() {} }, expectedOutputs: []string{ "Would have created a Pull Request with:", `title: my title`, `draft: false`, `base: master`, `head: feature`, `maintainerCanModify: false`, `body:`, `my body`, ``, }, expectedErrOut: "", }, { name: "dry-run-nontty-with-all-opts", tty: false, setup: func(opts *CreateOptions, t *testing.T) func() { opts.TitleProvided = true opts.BodyProvided = true opts.Title = "TITLE" opts.Body = "BODY" opts.BaseBranch = "trunk" opts.HeadBranch = "feature" opts.Assignees = []string{"monalisa"} opts.Labels = []string{"bug", "todo"} opts.Projects = []string{"roadmap"} opts.Reviewers = []string{"hubot", "monalisa", "/core", "/robots"} opts.Milestone = "big one.oh" opts.DryRun = true return func() {} }, httpStubs: func(reg *httpmock.Registry, t *testing.T) { reg.Register( httpmock.GraphQL(`query RepositoryResolveMetadataIDs\b`), httpmock.StringResponse(` { "data": { "u000": { "login": "MonaLisa", "id": "MONAID" }, "u001": { "login": "hubot", "id": "HUBOTID" }, "repository": { "l000": { "name": "bug", "id": "BUGID" }, "l001": { "name": "TODO", "id": "TODOID" } }, "organization": { "t000": { "slug": "core", "id": "COREID" }, "t001": { "slug": "robots", "id": "ROBOTID" } } } } `)) reg.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 } } } } } `)) mockRetrieveProjects(t, reg) }, expectedOutputs: []string{ "Would have created a Pull Request with:", `title: TITLE`, `draft: false`, `base: trunk`, `head: feature`, `labels: bug, todo`, `reviewers: hubot, monalisa, /core, /robots`, `assignees: monalisa`, `milestones: big one.oh`, `projects: roadmap`, `maintainerCanModify: false`, `body:`, `BODY`, ``, }, expectedErrOut: "", }, { name: "dry-run-tty-with-default-base", tty: true, setup: func(opts *CreateOptions, t *testing.T) func() { opts.TitleProvided = true opts.BodyProvided = true opts.Title = "my title" opts.Body = "my body" opts.HeadBranch = "feature" opts.DryRun = true return func() {} }, expectedOutputs: []string{ `Would have created a Pull Request with:`, `Title: my title`, `Draft: false`, `Base: master`, `Head: feature`, `MaintainerCanModify: false`, `Body:`, ``, ` my body `, ``, ``, }, expectedErrOut: heredoc.Doc(` Dry Running pull request for feature into master in OWNER/REPO `), }, { name: "dry-run-tty-with-all-opts", tty: true, setup: func(opts *CreateOptions, t *testing.T) func() { opts.TitleProvided = true opts.BodyProvided = true opts.Title = "TITLE" opts.Body = "BODY" opts.BaseBranch = "trunk" opts.HeadBranch = "feature" opts.Assignees = []string{"monalisa"} opts.Labels = []string{"bug", "todo"} opts.Projects = []string{"roadmap"} opts.Reviewers = []string{"hubot", "monalisa", "/core", "/robots"} opts.Milestone = "big one.oh" opts.DryRun = true return func() {} }, httpStubs: func(reg *httpmock.Registry, t *testing.T) { reg.Register( httpmock.GraphQL(`query RepositoryResolveMetadataIDs\b`), httpmock.StringResponse(` { "data": { "u000": { "login": "MonaLisa", "id": "MONAID" }, "u001": { "login": "hubot", "id": "HUBOTID" }, "repository": { "l000": { "name": "bug", "id": "BUGID" }, "l001": { "name": "TODO", "id": "TODOID" } }, "organization": { "t000": { "slug": "core", "id": "COREID" }, "t001": { "slug": "robots", "id": "ROBOTID" } } } } `)) reg.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 } } } } } `)) mockRetrieveProjects(t, reg) }, expectedOutputs: []string{ `Would have created a Pull Request with:`, `Title: TITLE`, `Draft: false`, `Base: trunk`, `Head: feature`, `Labels: bug, todo`, `Reviewers: hubot, monalisa, /core, /robots`, `Assignees: monalisa`, `Milestones: big one.oh`, `Projects: roadmap`, `MaintainerCanModify: false`, `Body:`, ``, ` BODY `, ``, ``, }, expectedErrOut: heredoc.Doc(` Dry Running pull request for feature into trunk in OWNER/REPO `), }, { name: "dry-run-tty-with-empty-body", tty: true, setup: func(opts *CreateOptions, t *testing.T) func() { opts.TitleProvided = true opts.BodyProvided = true opts.Title = "TITLE" opts.Body = "" opts.HeadBranch = "feature" opts.DryRun = true return func() {} }, expectedOut: heredoc.Doc(` Would have created a Pull Request with: Title: TITLE Draft: false Base: master Head: feature MaintainerCanModify: false Body: No description provided `), expectedErrOut: heredoc.Doc(` Dry Running pull request for feature into master in OWNER/REPO `), }, { name: "survey", tty: true, setup: func(opts *CreateOptions, t *testing.T) func() { opts.TitleProvided = true opts.BodyProvided = true opts.Title = "my title" opts.Body = "my body" return func() {} }, httpStubs: func(reg *httpmock.Registry, t *testing.T) { reg.StubRepoResponse("OWNER", "REPO") reg.Register( httpmock.GraphQL(`query UserCurrent\b`), httpmock.StringResponse(`{"data": {"viewer": {"login": "OWNER"} } }`)) reg.Register( httpmock.GraphQL(`mutation PullRequestCreate\b`), httpmock.GraphQLMutation(` { "data": { "createPullRequest": { "pullRequest": { "URL": "https://github.com/OWNER/REPO/pull/12" } } } }`, func(input map[string]interface{}) { assert.Equal(t, "REPOID", input["repositoryId"].(string)) assert.Equal(t, "my title", input["title"].(string)) assert.Equal(t, "my body", input["body"].(string)) assert.Equal(t, "master", input["baseRefName"].(string)) assert.Equal(t, "feature", input["headRefName"].(string)) assert.Equal(t, false, input["draft"].(bool)) })) }, cmdStubs: func(cs *run.CommandStubber) { cs.Register(`git config --get-regexp.+branch\\\.feature\\\.`, 0, "") cs.Register(`git show-ref --verify -- HEAD refs/remotes/origin/feature`, 0, "") cs.Register(`git push --set-upstream origin HEAD:refs/heads/feature`, 0, "") }, promptStubs: func(pm *prompter.PrompterMock) { pm.SelectFunc = func(p, _ string, opts []string) (int, error) { if p == "Where should we push the 'feature' branch?" { return 0, nil } else { return -1, prompter.NoSuchPromptErr(p) } } }, expectedOut: "https://github.com/OWNER/REPO/pull/12\n", expectedErrOut: "\nCreating pull request for feature into master in OWNER/REPO\n\n", }, { name: "project v2", tty: true, setup: func(opts *CreateOptions, t *testing.T) func() { opts.TitleProvided = true opts.BodyProvided = true opts.Title = "my title" opts.Body = "my body" opts.Projects = []string{"RoadmapV2"} return func() {} }, httpStubs: func(reg *httpmock.Registry, t *testing.T) { reg.StubRepoResponse("OWNER", "REPO") reg.Register( httpmock.GraphQL(`query UserCurrent\b`), httpmock.StringResponse(`{"data": {"viewer": {"login": "OWNER"} } }`)) mockRetrieveProjects(t, reg) reg.Register( httpmock.GraphQL(`mutation PullRequestCreate\b`), httpmock.GraphQLMutation(` { "data": { "createPullRequest": { "pullRequest": { "id": "PullRequest#1", "URL": "https://github.com/OWNER/REPO/pull/12" } } } } `, func(input map[string]interface{}) { assert.Equal(t, "REPOID", input["repositoryId"].(string)) assert.Equal(t, "my title", input["title"].(string)) assert.Equal(t, "my body", input["body"].(string)) assert.Equal(t, "master", input["baseRefName"].(string)) assert.Equal(t, "feature", input["headRefName"].(string)) assert.Equal(t, false, input["draft"].(bool)) })) reg.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":"PullRequest#1","projectId":"ROADMAPV2ID"}}` assert.Equal(t, expectedMutations, mutations) assert.Equal(t, expectedVariables, string(variables)) })) }, cmdStubs: func(cs *run.CommandStubber) { cs.Register(`git config --get-regexp.+branch\\\.feature\\\.`, 0, "") cs.Register(`git show-ref --verify -- HEAD refs/remotes/origin/feature`, 0, "") cs.Register(`git push --set-upstream origin HEAD:refs/heads/feature`, 0, "") }, promptStubs: func(pm *prompter.PrompterMock) { pm.SelectFunc = func(p, _ string, opts []string) (int, error) { if p == "Where should we push the 'feature' branch?" { return 0, nil } else { return -1, prompter.NoSuchPromptErr(p) } } }, expectedOut: "https://github.com/OWNER/REPO/pull/12\n", expectedErrOut: "\nCreating pull request for feature into master in OWNER/REPO\n\n", }, { name: "no maintainer modify", tty: true, setup: func(opts *CreateOptions, t *testing.T) func() { opts.TitleProvided = true opts.BodyProvided = true opts.Title = "my title" opts.Body = "my body" return func() {} }, httpStubs: func(reg *httpmock.Registry, t *testing.T) { reg.StubRepoResponse("OWNER", "REPO") reg.Register( httpmock.GraphQL(`query UserCurrent\b`), httpmock.StringResponse(`{"data": {"viewer": {"login": "OWNER"} } }`)) reg.Register( httpmock.GraphQL(`mutation PullRequestCreate\b`), httpmock.GraphQLMutation(` { "data": { "createPullRequest": { "pullRequest": { "URL": "https://github.com/OWNER/REPO/pull/12" } } } } `, func(input map[string]interface{}) { assert.Equal(t, false, input["maintainerCanModify"].(bool)) assert.Equal(t, "REPOID", input["repositoryId"].(string)) assert.Equal(t, "my title", input["title"].(string)) assert.Equal(t, "my body", input["body"].(string)) assert.Equal(t, "master", input["baseRefName"].(string)) assert.Equal(t, "feature", input["headRefName"].(string)) })) }, cmdStubs: func(cs *run.CommandStubber) { cs.Register(`git config --get-regexp.+branch\\\.feature\\\.`, 0, "") cs.Register(`git show-ref --verify -- HEAD refs/remotes/origin/feature`, 0, "") cs.Register(`git push --set-upstream origin HEAD:refs/heads/feature`, 0, "") }, promptStubs: func(pm *prompter.PrompterMock) { pm.SelectFunc = func(p, _ string, opts []string) (int, error) { if p == "Where should we push the 'feature' branch?" { return 0, nil } else { return -1, prompter.NoSuchPromptErr(p) } } }, expectedOut: "https://github.com/OWNER/REPO/pull/12\n", expectedErrOut: "\nCreating pull request for feature into master in OWNER/REPO\n\n", }, { name: "create fork", tty: true, setup: func(opts *CreateOptions, t *testing.T) func() { opts.TitleProvided = true opts.BodyProvided = true opts.Title = "title" opts.Body = "body" return func() {} }, httpStubs: func(reg *httpmock.Registry, t *testing.T) { reg.StubRepoResponse("OWNER", "REPO") reg.Register( httpmock.GraphQL(`query UserCurrent\b`), httpmock.StringResponse(`{"data": {"viewer": {"login": "monalisa"} } }`)) reg.Register( httpmock.REST("POST", "repos/OWNER/REPO/forks"), httpmock.StatusStringResponse(201, ` { "node_id": "NODEID", "name": "REPO", "owner": {"login": "monalisa"} }`)) reg.Register( httpmock.GraphQL(`mutation PullRequestCreate\b`), httpmock.GraphQLMutation(` { "data": { "createPullRequest": { "pullRequest": { "URL": "https://github.com/OWNER/REPO/pull/12" }}}}`, func(input map[string]interface{}) { assert.Equal(t, "REPOID", input["repositoryId"].(string)) assert.Equal(t, "master", input["baseRefName"].(string)) assert.Equal(t, "monalisa:feature", input["headRefName"].(string)) })) }, cmdStubs: func(cs *run.CommandStubber) { cs.Register(`git config --get-regexp.+branch\\\.feature\\\.`, 0, "") cs.Register(`git show-ref --verify -- HEAD refs/remotes/origin/feature`, 0, "") cs.Register("git remote rename origin upstream", 0, "") cs.Register(`git remote add origin https://github.com/monalisa/REPO.git`, 0, "") cs.Register(`git push --set-upstream origin HEAD:refs/heads/feature`, 0, "") }, promptStubs: func(pm *prompter.PrompterMock) { pm.SelectFunc = func(p, _ string, opts []string) (int, error) { if p == "Where should we push the 'feature' branch?" { return prompter.IndexFor(opts, "Create a fork of OWNER/REPO") } else { return -1, prompter.NoSuchPromptErr(p) } } }, expectedOut: "https://github.com/OWNER/REPO/pull/12\n", expectedErrOut: "\nCreating pull request for monalisa:feature into master in OWNER/REPO\n\nChanged OWNER/REPO remote to \"upstream\"\nAdded monalisa/REPO as remote \"origin\"\n", }, { name: "pushed to non base repo", tty: true, setup: func(opts *CreateOptions, t *testing.T) func() { opts.TitleProvided = true opts.BodyProvided = true opts.Title = "title" opts.Body = "body" opts.Remotes = func() (context.Remotes, error) { return context.Remotes{ { Remote: &git.Remote{ Name: "upstream", Resolved: "base", }, Repo: ghrepo.New("OWNER", "REPO"), }, { Remote: &git.Remote{ Name: "origin", Resolved: "base", }, Repo: ghrepo.New("monalisa", "REPO"), }, }, nil } return func() {} }, httpStubs: func(reg *httpmock.Registry, t *testing.T) { reg.Register( httpmock.GraphQL(`mutation PullRequestCreate\b`), httpmock.GraphQLMutation(` { "data": { "createPullRequest": { "pullRequest": { "URL": "https://github.com/OWNER/REPO/pull/12" } } } }`, func(input map[string]interface{}) { assert.Equal(t, "REPOID", input["repositoryId"].(string)) assert.Equal(t, "master", input["baseRefName"].(string)) assert.Equal(t, "monalisa:feature", input["headRefName"].(string)) })) }, cmdStubs: func(cs *run.CommandStubber) { cs.Register(`git config --get-regexp \^branch\\\.feature\\\.`, 1, "") // determineTrackingBranch cs.Register("git show-ref --verify", 0, heredoc.Doc(` deadbeef HEAD deadb00f refs/remotes/upstream/feature deadbeef refs/remotes/origin/feature`)) // determineTrackingBranch }, expectedOut: "https://github.com/OWNER/REPO/pull/12\n", expectedErrOut: "\nCreating pull request for monalisa:feature into master in OWNER/REPO\n\n", }, { name: "pushed to different branch name", tty: true, setup: func(opts *CreateOptions, t *testing.T) func() { opts.TitleProvided = true opts.BodyProvided = true opts.Title = "title" opts.Body = "body" return func() {} }, httpStubs: func(reg *httpmock.Registry, t *testing.T) { reg.Register( httpmock.GraphQL(`mutation PullRequestCreate\b`), httpmock.GraphQLMutation(` { "data": { "createPullRequest": { "pullRequest": { "URL": "https://github.com/OWNER/REPO/pull/12" } } } } `, func(input map[string]interface{}) { assert.Equal(t, "REPOID", input["repositoryId"].(string)) assert.Equal(t, "master", input["baseRefName"].(string)) assert.Equal(t, "my-feat2", input["headRefName"].(string)) })) }, cmdStubs: func(cs *run.CommandStubber) { cs.Register(`git config --get-regexp \^branch\\\.feature\\\.`, 0, heredoc.Doc(` branch.feature.remote origin branch.feature.merge refs/heads/my-feat2 `)) // determineTrackingBranch cs.Register("git show-ref --verify", 0, heredoc.Doc(` deadbeef HEAD deadbeef refs/remotes/origin/my-feat2 `)) // determineTrackingBranch }, expectedOut: "https://github.com/OWNER/REPO/pull/12\n", expectedErrOut: "\nCreating pull request for my-feat2 into master in OWNER/REPO\n\n", }, { name: "non legacy template", tty: true, setup: func(opts *CreateOptions, t *testing.T) func() { opts.TitleProvided = true opts.Title = "my title" opts.HeadBranch = "feature" opts.RootDirOverride = "./fixtures/repoWithNonLegacyPRTemplates" return func() {} }, httpStubs: func(reg *httpmock.Registry, t *testing.T) { reg.Register( httpmock.GraphQL(`query PullRequestTemplates\b`), httpmock.StringResponse(` { "data": { "repository": { "pullRequestTemplates": [ { "filename": "template1", "body": "this is a bug" }, { "filename": "template2", "body": "this is a enhancement" } ] } } }`)) reg.Register( httpmock.GraphQL(`mutation PullRequestCreate\b`), httpmock.GraphQLMutation(` { "data": { "createPullRequest": { "pullRequest": { "URL": "https://github.com/OWNER/REPO/pull/12" } } } } `, func(input map[string]interface{}) { assert.Equal(t, "my title", input["title"].(string)) assert.Equal(t, "- **commit 1**\n- **commit 0**\n\nthis is a bug", input["body"].(string)) })) }, cmdStubs: func(cs *run.CommandStubber) { cs.Register(`git( .+)? log( .+)? origin/master\.\.\.feature`, 0, "d3476a1\u0000commit 0\u0000\u0000\n7a6ea13\u0000commit 1\u0000\u0000") }, promptStubs: func(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 0, nil case "Choose a template": return prompter.IndexFor(opts, "template1") default: return -1, prompter.NoSuchPromptErr(p) } } }, expectedOut: "https://github.com/OWNER/REPO/pull/12\n", expectedErrOut: "\nCreating pull request for feature into master in OWNER/REPO\n\n", }, { name: "metadata", tty: true, setup: func(opts *CreateOptions, t *testing.T) func() { opts.TitleProvided = true opts.Title = "TITLE" opts.BodyProvided = true opts.Body = "BODY" opts.HeadBranch = "feature" opts.Assignees = []string{"monalisa"} opts.Labels = []string{"bug", "todo"} opts.Projects = []string{"roadmap"} opts.Reviewers = []string{"hubot", "monalisa", "/core", "/robots"} opts.Milestone = "big one.oh" return func() {} }, httpStubs: func(reg *httpmock.Registry, t *testing.T) { reg.Register( httpmock.GraphQL(`query RepositoryResolveMetadataIDs\b`), httpmock.StringResponse(` { "data": { "u000": { "login": "MonaLisa", "id": "MONAID" }, "u001": { "login": "hubot", "id": "HUBOTID" }, "repository": { "l000": { "name": "bug", "id": "BUGID" }, "l001": { "name": "TODO", "id": "TODOID" } }, "organization": { "t000": { "slug": "core", "id": "COREID" }, "t001": { "slug": "robots", "id": "ROBOTID" } } } } `)) reg.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 } } } } } `)) mockRetrieveProjects(t, reg) reg.Register( httpmock.GraphQL(`mutation PullRequestCreate\b`), httpmock.GraphQLMutation(` { "data": { "createPullRequest": { "pullRequest": { "id": "NEWPULLID", "URL": "https://github.com/OWNER/REPO/pull/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) } if v, ok := inputs["userIds"]; ok { t.Errorf("did not expect userIds: %v", v) } })) reg.Register( httpmock.GraphQL(`mutation PullRequestCreateMetadata\b`), httpmock.GraphQLMutation(` { "data": { "updatePullRequest": { "clientMutationId": "" } } } `, func(inputs map[string]interface{}) { assert.Equal(t, "NEWPULLID", inputs["pullRequestId"]) 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"]) })) reg.Register( httpmock.GraphQL(`mutation PullRequestCreateRequestReviews\b`), httpmock.GraphQLMutation(` { "data": { "requestReviews": { "clientMutationId": "" } } } `, func(inputs map[string]interface{}) { assert.Equal(t, "NEWPULLID", inputs["pullRequestId"]) assert.Equal(t, []interface{}{"HUBOTID", "MONAID"}, inputs["userIds"]) assert.Equal(t, []interface{}{"COREID", "ROBOTID"}, inputs["teamIds"]) assert.Equal(t, true, inputs["union"]) })) }, expectedOut: "https://github.com/OWNER/REPO/pull/12\n", expectedErrOut: "\nCreating pull request for feature into master in OWNER/REPO\n\n", }, { name: "already exists", tty: true, setup: func(opts *CreateOptions, t *testing.T) func() { opts.TitleProvided = true opts.BodyProvided = true opts.Title = "title" opts.Body = "body" opts.HeadBranch = "feature" opts.Finder = shared.NewMockFinder("feature", &api.PullRequest{URL: "https://github.com/OWNER/REPO/pull/123"}, ghrepo.New("OWNER", "REPO")) return func() {} }, wantErr: "a pull request for branch \"feature\" into branch \"master\" already exists:\nhttps://github.com/OWNER/REPO/pull/123", }, { name: "web", tty: true, setup: func(opts *CreateOptions, t *testing.T) func() { opts.WebMode = true return func() {} }, httpStubs: func(reg *httpmock.Registry, t *testing.T) { reg.StubRepoResponse("OWNER", "REPO") reg.Register( httpmock.GraphQL(`query UserCurrent\b`), httpmock.StringResponse(`{"data": {"viewer": {"login": "OWNER"} } }`)) }, cmdStubs: func(cs *run.CommandStubber) { cs.Register(`git config --get-regexp.+branch\\\.feature\\\.`, 0, "") cs.Register(`git show-ref --verify -- HEAD refs/remotes/origin/feature`, 0, "") cs.Register(`git( .+)? log( .+)? origin/master\.\.\.feature`, 0, "") cs.Register(`git push --set-upstream origin HEAD:refs/heads/feature`, 0, "") }, promptStubs: func(pm *prompter.PrompterMock) { pm.SelectFunc = func(p, _ string, opts []string) (int, error) { if p == "Where should we push the 'feature' branch?" { return 0, nil } else { return -1, prompter.NoSuchPromptErr(p) } } }, expectedErrOut: "Opening github.com/OWNER/REPO/compare/master...feature in your browser.\n", expectedBrowse: "https://github.com/OWNER/REPO/compare/master...feature?body=&expand=1", }, { name: "web project", tty: true, setup: func(opts *CreateOptions, t *testing.T) func() { opts.WebMode = true opts.Projects = []string{"Triage"} return func() {} }, httpStubs: func(reg *httpmock.Registry, t *testing.T) { reg.StubRepoResponse("OWNER", "REPO") reg.Register( httpmock.GraphQL(`query UserCurrent\b`), httpmock.StringResponse(`{"data": {"viewer": {"login": "OWNER"} } }`)) mockRetrieveProjects(t, reg) }, cmdStubs: func(cs *run.CommandStubber) { cs.Register(`git config --get-regexp.+branch\\\.feature\\\.`, 0, "") cs.Register(`git show-ref --verify -- HEAD refs/remotes/origin/feature`, 0, "") cs.Register(`git( .+)? log( .+)? origin/master\.\.\.feature`, 0, "") cs.Register(`git push --set-upstream origin HEAD:refs/heads/feature`, 0, "") }, promptStubs: func(pm *prompter.PrompterMock) { pm.SelectFunc = func(p, _ string, opts []string) (int, error) { if p == "Where should we push the 'feature' branch?" { return 0, nil } else { return -1, prompter.NoSuchPromptErr(p) } } }, expectedErrOut: "Opening github.com/OWNER/REPO/compare/master...feature in your browser.\n", expectedBrowse: "https://github.com/OWNER/REPO/compare/master...feature?body=&expand=1&projects=ORG%2F1", }, { name: "draft", tty: true, setup: func(opts *CreateOptions, t *testing.T) func() { opts.TitleProvided = true opts.Title = "my title" opts.HeadBranch = "feature" return func() {} }, httpStubs: func(reg *httpmock.Registry, t *testing.T) { reg.Register( httpmock.GraphQL(`query PullRequestTemplates\b`), httpmock.StringResponse(` { "data": { "repository": { "pullRequestTemplates": [ { "filename": "template1", "body": "this is a bug" }, { "filename": "template2", "body": "this is a enhancement" } ] } } }`), ) reg.Register( httpmock.GraphQL(`mutation PullRequestCreate\b`), httpmock.GraphQLMutation(` { "data": { "createPullRequest": { "pullRequest": { "URL": "https://github.com/OWNER/REPO/pull/12" } } } } `, func(input map[string]interface{}) { assert.Equal(t, true, input["draft"].(bool)) })) }, cmdStubs: func(cs *run.CommandStubber) { cs.Register(`git -c log.ShowSignature=false log --pretty=format:%H%x00%s%x00%b%x00 --cherry origin/master...feature`, 0, "") cs.Register(`git rev-parse --show-toplevel`, 0, "") }, promptStubs: func(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 as draft") case "Choose a template": return 0, nil default: return -1, prompter.NoSuchPromptErr(p) } } }, expectedOut: "https://github.com/OWNER/REPO/pull/12\n", expectedErrOut: "\nCreating pull request for feature into master in OWNER/REPO\n\n", }, { name: "recover", tty: true, httpStubs: func(reg *httpmock.Registry, t *testing.T) { reg.Register( httpmock.GraphQL(`query RepositoryResolveMetadataIDs\b`), httpmock.StringResponse(` { "data": { "u000": { "login": "jillValentine", "id": "JILLID" }, "repository": {}, "organization": {} } } `)) reg.Register( httpmock.GraphQL(`mutation PullRequestCreateRequestReviews\b`), httpmock.GraphQLMutation(` { "data": { "requestReviews": { "clientMutationId": "" } } } `, func(inputs map[string]interface{}) { assert.Equal(t, []interface{}{"JILLID"}, inputs["userIds"]) })) reg.Register( httpmock.GraphQL(`mutation PullRequestCreate\b`), httpmock.GraphQLMutation(` { "data": { "createPullRequest": { "pullRequest": { "URL": "https://github.com/OWNER/REPO/pull/12" } } } } `, func(input map[string]interface{}) { assert.Equal(t, "recovered title", input["title"].(string)) assert.Equal(t, "recovered body", input["body"].(string)) })) }, cmdStubs: func(cs *run.CommandStubber) { cs.Register(`git( .+)? log( .+)? origin/master\.\.\.feature`, 0, "") }, promptStubs: func(pm *prompter.PrompterMock) { pm.InputFunc = func(p, d string) (string, error) { if p == "Title" { 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 0, nil } else { return -1, prompter.NoSuchPromptErr(p) } } }, setup: func(opts *CreateOptions, t *testing.T) func() { tmpfile, err := os.CreateTemp(t.TempDir(), "testrecover*") assert.NoError(t, err) state := shared.IssueMetadataState{ Title: "recovered title", Body: "recovered body", Reviewers: []string{"jillValentine"}, } data, err := json.Marshal(state) assert.NoError(t, err) _, err = tmpfile.Write(data) assert.NoError(t, err) opts.RecoverFile = tmpfile.Name() opts.HeadBranch = "feature" return func() { tmpfile.Close() } }, expectedOut: "https://github.com/OWNER/REPO/pull/12\n", expectedErrOut: "\nCreating pull request for feature into master in OWNER/REPO\n\n", }, { name: "web long URL", cmdStubs: func(cs *run.CommandStubber) { cs.Register(`git( .+)? log( .+)? origin/master\.\.\.feature`, 0, "") }, setup: func(opts *CreateOptions, t *testing.T) func() { longBody := make([]byte, 9216) opts.Body = string(longBody) opts.BodyProvided = true opts.WebMode = true opts.HeadBranch = "feature" return func() {} }, wantErr: "cannot open in browser: maximum URL length exceeded", }, { name: "no local git repo", setup: func(opts *CreateOptions, t *testing.T) func() { opts.Title = "My PR" opts.TitleProvided = true opts.Body = "" opts.BodyProvided = true opts.HeadBranch = "feature" opts.RepoOverride = "OWNER/REPO" opts.Remotes = func() (context.Remotes, error) { return nil, errors.New("not a git repository") } return func() {} }, httpStubs: func(reg *httpmock.Registry, t *testing.T) { reg.Register( httpmock.GraphQL(`mutation PullRequestCreate\b`), httpmock.StringResponse(` { "data": { "createPullRequest": { "pullRequest": { "URL": "https://github.com/OWNER/REPO/pull/12" } } } } `)) }, expectedOut: "https://github.com/OWNER/REPO/pull/12\n", }, { name: "single commit title and body are used", tty: true, setup: func(opts *CreateOptions, t *testing.T) func() { opts.HeadBranch = "feature" return func() {} }, cmdStubs: func(cs *run.CommandStubber) { cs.Register( "git -c log.ShowSignature=false log --pretty=format:%H%x00%s%x00%b%x00 --cherry origin/master...feature", 0, "3a9b48085046d156c5acce8f3b3a0532cd706a4a\u0000first commit of pr\u0000first commit description\u0000\n", ) cs.Register(`git rev-parse --show-toplevel`, 0, "") }, promptStubs: func(pm *prompter.PrompterMock) { pm.SelectFunc = func(p, _ string, opts []string) (int, error) { if p == "Where should we push the 'feature' branch?" { return 0, nil } else { return -1, prompter.NoSuchPromptErr(p) } } pm.InputFunc = func(p, d string) (string, error) { if p == "Title" { return d, nil } else if p == "Body" { 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 0, nil } else { return -1, prompter.NoSuchPromptErr(p) } } }, httpStubs: func(reg *httpmock.Registry, t *testing.T) { reg.Register( httpmock.GraphQL(`query PullRequestTemplates\b`), httpmock.StringResponse(`{ "data": { "repository": { "pullRequestTemplates": [] } } }`), ) reg.Register( httpmock.GraphQL(`mutation PullRequestCreate\b`), httpmock.GraphQLMutation(` { "data": { "createPullRequest": { "pullRequest": { "URL": "https://github.com/OWNER/REPO/pull/12" } } } } `, func(input map[string]interface{}) { assert.Equal(t, "first commit of pr", input["title"], "pr title should be first commit message") assert.Equal(t, "first commit description", input["body"], "pr body should be first commit description") }, ), ) }, expectedOut: "https://github.com/OWNER/REPO/pull/12\n", expectedErrOut: "\nCreating pull request for feature into master in OWNER/REPO\n\n", }, { name: "fill-first flag provided", tty: true, setup: func(opts *CreateOptions, t *testing.T) func() { opts.FillFirst = true opts.HeadBranch = "feature" return func() {} }, cmdStubs: func(cs *run.CommandStubber) { cs.Register( "git -c log.ShowSignature=false log --pretty=format:%H%x00%s%x00%b%x00 --cherry origin/master...feature", 0, "56b6f8bb7c9e3a30093cd17e48934ce354148e80\u0000second commit of pr\u0000\u0000\n"+ "3a9b48085046d156c5acce8f3b3a0532cd706a4a\u0000first commit of pr\u0000first commit description\u0000\n", ) }, httpStubs: func(reg *httpmock.Registry, t *testing.T) { reg.Register( httpmock.GraphQL(`mutation PullRequestCreate\b`), httpmock.GraphQLMutation(` { "data": { "createPullRequest": { "pullRequest": { "URL": "https://github.com/OWNER/REPO/pull/12" } } } } `, func(input map[string]interface{}) { assert.Equal(t, "first commit of pr", input["title"], "pr title should be first commit message") assert.Equal(t, "first commit description", input["body"], "pr body should be first commit description") }, ), ) }, expectedOut: "https://github.com/OWNER/REPO/pull/12\n", expectedErrOut: "\nCreating pull request for feature into master in OWNER/REPO\n\n", }, { name: "fillverbose flag provided", tty: true, setup: func(opts *CreateOptions, t *testing.T) func() { opts.FillVerbose = true opts.HeadBranch = "feature" return func() {} }, cmdStubs: func(cs *run.CommandStubber) { cs.Register( "git -c log.ShowSignature=false log --pretty=format:%H%x00%s%x00%b%x00 --cherry origin/master...feature", 0, "56b6f8bb7c9e3a30093cd17e48934ce354148e80\u0000second commit of pr\u0000second commit description\u0000\n"+ "3a9b48085046d156c5acce8f3b3a0532cd706a4a\u0000first commit of pr\u0000first commit with super long description, with super long description, with super long description, with super long description.\u0000\n", ) }, httpStubs: func(reg *httpmock.Registry, t *testing.T) { reg.Register( httpmock.GraphQL(`mutation PullRequestCreate\b`), httpmock.GraphQLMutation(` { "data": { "createPullRequest": { "pullRequest": { "URL": "https://github.com/OWNER/REPO/pull/12" } } } } `, func(input map[string]interface{}) { assert.Equal(t, "feature", input["title"], "pr title should be branch name") assert.Equal(t, "- **first commit of pr**\n first commit with super long description, with super long description, with super long description, with super long description.\n\n- **second commit of pr**\n second commit description", input["body"], "pr body should be commits msg+body") }, ), ) }, expectedOut: "https://github.com/OWNER/REPO/pull/12\n", expectedErrOut: "\nCreating pull request for feature into master in OWNER/REPO\n\n", }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { branch := "feature" reg := &httpmock.Registry{} reg.StubRepoInfoResponse("OWNER", "REPO", "master") defer reg.Verify(t) if tt.httpStubs != nil { tt.httpStubs(reg, t) } pm := &prompter.PrompterMock{} if tt.promptStubs != nil { tt.promptStubs(pm) } cs, cmdTeardown := run.Stub() defer cmdTeardown(t) cs.Register(`git status --porcelain`, 0, "") if tt.cmdStubs != nil { tt.cmdStubs(cs) } opts := CreateOptions{} opts.Prompter = pm ios, _, stdout, stderr := iostreams.Test() // TODO do i need to bother with this ios.SetStdoutTTY(tt.tty) ios.SetStdinTTY(tt.tty) ios.SetStderrTTY(tt.tty) browser := &browser.Stub{} opts.IO = ios opts.Browser = browser opts.HttpClient = func() (*http.Client, error) { return &http.Client{Transport: reg}, nil } opts.Config = func() (config.Config, error) { return config.NewBlankConfig(), nil } opts.Remotes = func() (context.Remotes, error) { return context.Remotes{ { Remote: &git.Remote{ Name: "origin", Resolved: "base", }, Repo: ghrepo.New("OWNER", "REPO"), }, }, nil } opts.Branch = func() (string, error) { return branch, nil } opts.Finder = shared.NewMockFinder(branch, nil, nil) opts.GitClient = &git.Client{ GhPath: "some/path/gh", GitPath: "some/path/git", } cleanSetup := func() {} if tt.setup != nil { cleanSetup = tt.setup(&opts, t) } defer cleanSetup() err := createRun(&opts) output := &test.CmdOut{ OutBuf: stdout, ErrBuf: stderr, BrowsedURL: browser.BrowsedURL(), } if tt.wantErr != "" { assert.EqualError(t, err, tt.wantErr) } else { assert.NoError(t, err) if tt.expectedOut != "" { assert.Equal(t, tt.expectedOut, output.String()) } if len(tt.expectedOutputs) > 0 { assert.Equal(t, tt.expectedOutputs, strings.Split(output.String(), "\n")) } assert.Equal(t, tt.expectedErrOut, output.Stderr()) assert.Equal(t, tt.expectedBrowse, output.BrowsedURL) } }) } } func Test_determineTrackingBranch(t *testing.T) { tests := []struct { name string cmdStubs func(*run.CommandStubber) remotes context.Remotes assert func(ref *git.TrackingRef, t *testing.T) }{ { name: "empty", cmdStubs: func(cs *run.CommandStubber) { cs.Register(`git config --get-regexp.+branch\\\.feature\\\.`, 0, "") cs.Register(`git show-ref --verify -- HEAD`, 0, "abc HEAD") }, assert: func(ref *git.TrackingRef, t *testing.T) { assert.Nil(t, ref) }, }, { name: "no match", cmdStubs: func(cs *run.CommandStubber) { cs.Register(`git config --get-regexp.+branch\\\.feature\\\.`, 0, "") cs.Register("git show-ref --verify -- HEAD refs/remotes/origin/feature refs/remotes/upstream/feature", 0, "abc HEAD\nbca refs/remotes/origin/feature") }, remotes: context.Remotes{ &context.Remote{ Remote: &git.Remote{Name: "origin"}, Repo: ghrepo.New("hubot", "Spoon-Knife"), }, &context.Remote{ Remote: &git.Remote{Name: "upstream"}, Repo: ghrepo.New("octocat", "Spoon-Knife"), }, }, assert: func(ref *git.TrackingRef, t *testing.T) { assert.Nil(t, ref) }, }, { name: "match", cmdStubs: func(cs *run.CommandStubber) { cs.Register(`git config --get-regexp.+branch\\\.feature\\\.`, 0, "") cs.Register(`git show-ref --verify -- HEAD refs/remotes/origin/feature refs/remotes/upstream/feature$`, 0, heredoc.Doc(` deadbeef HEAD deadb00f refs/remotes/origin/feature deadbeef refs/remotes/upstream/feature `)) }, remotes: context.Remotes{ &context.Remote{ Remote: &git.Remote{Name: "origin"}, Repo: ghrepo.New("hubot", "Spoon-Knife"), }, &context.Remote{ Remote: &git.Remote{Name: "upstream"}, Repo: ghrepo.New("octocat", "Spoon-Knife"), }, }, assert: func(ref *git.TrackingRef, t *testing.T) { assert.Equal(t, "upstream", ref.RemoteName) assert.Equal(t, "feature", ref.BranchName) }, }, { name: "respect tracking config", cmdStubs: func(cs *run.CommandStubber) { cs.Register(`git config --get-regexp.+branch\\\.feature\\\.`, 0, heredoc.Doc(` branch.feature.remote origin branch.feature.merge refs/heads/great-feat `)) cs.Register(`git show-ref --verify -- HEAD refs/remotes/origin/great-feat refs/remotes/origin/feature$`, 0, heredoc.Doc(` deadbeef HEAD deadb00f refs/remotes/origin/feature `)) }, remotes: context.Remotes{ &context.Remote{ Remote: &git.Remote{Name: "origin"}, Repo: ghrepo.New("hubot", "Spoon-Knife"), }, }, assert: func(ref *git.TrackingRef, t *testing.T) { assert.Nil(t, ref) }, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { cs, cmdTeardown := run.Stub() defer cmdTeardown(t) tt.cmdStubs(cs) gitClient := &git.Client{ GhPath: "some/path/gh", GitPath: "some/path/git", } ref := determineTrackingBranch(gitClient, tt.remotes, "feature") tt.assert(ref, t) }) } } func Test_generateCompareURL(t *testing.T) { tests := []struct { name string ctx CreateContext state shared.IssueMetadataState want string wantErr bool }{ { name: "basic", ctx: CreateContext{ BaseRepo: api.InitRepoHostname(&api.Repository{Name: "REPO", Owner: api.RepositoryOwner{Login: "OWNER"}}, "github.com"), BaseBranch: "main", HeadBranchLabel: "feature", }, want: "https://github.com/OWNER/REPO/compare/main...feature?body=&expand=1", wantErr: false, }, { name: "with labels", ctx: CreateContext{ BaseRepo: api.InitRepoHostname(&api.Repository{Name: "REPO", Owner: api.RepositoryOwner{Login: "OWNER"}}, "github.com"), BaseBranch: "a", HeadBranchLabel: "b", }, state: shared.IssueMetadataState{ Labels: []string{"one", "two three"}, }, want: "https://github.com/OWNER/REPO/compare/a...b?body=&expand=1&labels=one%2Ctwo+three", wantErr: false, }, { name: "'/'s in branch names/labels are percent-encoded", ctx: CreateContext{ BaseRepo: api.InitRepoHostname(&api.Repository{Name: "REPO", Owner: api.RepositoryOwner{Login: "OWNER"}}, "github.com"), BaseBranch: "main/trunk", HeadBranchLabel: "owner:feature", }, want: "https://github.com/OWNER/REPO/compare/main%2Ftrunk...owner:feature?body=&expand=1", wantErr: false, }, { name: "Any of !'(),; but none of $&+=@ and : in branch names/labels are percent-encoded ", /* - Technically, per section 3.3 of RFC 3986, none of !$&'()*+,;= (sub-delims) and :[]@ (part of gen-delims) in path segments are optionally percent-encoded, but url.PathEscape percent-encodes !'(),; anyway - !$&'()+,;=@ is a valid Git branch name—essentially RFC 3986 sub-delims without * and gen-delims without :/?#[] - : is GitHub separator between a fork name and a branch name - See https://github.com/golang/go/issues/27559. */ ctx: CreateContext{ BaseRepo: api.InitRepoHostname(&api.Repository{Name: "REPO", Owner: api.RepositoryOwner{Login: "OWNER"}}, "github.com"), BaseBranch: "main/trunk", HeadBranchLabel: "owner:!$&'()+,;=@", }, want: "https://github.com/OWNER/REPO/compare/main%2Ftrunk...owner:%21$&%27%28%29+%2C%3B=@?body=&expand=1", wantErr: false, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { got, err := generateCompareURL(tt.ctx, tt.state) if (err != nil) != tt.wantErr { t.Errorf("generateCompareURL() error = %v, wantErr %v", err, tt.wantErr) return } if got != tt.want { t.Errorf("generateCompareURL() = %v, want %v", got, tt.want) } }) } } func mockRetrieveProjects(_ *testing.T, reg *httpmock.Registry) { reg.Register( httpmock.GraphQL(`query RepositoryProjectList\b`), httpmock.StringResponse(` { "data": { "repository": { "projects": { "nodes": [ { "name": "Cleanup", "id": "CLEANUPID", "resourcePath": "/OWNER/REPO/projects/1" }, { "name": "Roadmap", "id": "ROADMAPID", "resourcePath": "/OWNER/REPO/projects/2" } ], "pageInfo": { "hasNextPage": false } } } } } `)) reg.Register( httpmock.GraphQL(`query RepositoryProjectV2List\b`), httpmock.StringResponse(` { "data": { "repository": { "projectsV2": { "nodes": [ { "title": "CleanupV2", "id": "CLEANUPV2ID", "resourcePath": "/OWNER/REPO/projects/3" }, { "title": "RoadmapV2", "id": "ROADMAPV2ID", "resourcePath": "/OWNER/REPO/projects/4" } ], "pageInfo": { "hasNextPage": false } } } } } `)) reg.Register( httpmock.GraphQL(`query OrganizationProjectList\b`), httpmock.StringResponse(` { "data": { "organization": { "projects": { "nodes": [ { "name": "Triage", "id": "TRIAGEID", "resourcePath": "/orgs/ORG/projects/1" } ], "pageInfo": { "hasNextPage": false } } } } } `)) reg.Register( httpmock.GraphQL(`query OrganizationProjectV2List\b`), httpmock.StringResponse(` { "data": { "organization": { "projectsV2": { "nodes": [ { "title": "TriageV2", "id": "TRIAGEV2ID", "resourcePath": "/orgs/ORG/projects/2" } ], "pageInfo": { "hasNextPage": false } } } } } `)) reg.Register( httpmock.GraphQL(`query UserProjectV2List\b`), httpmock.StringResponse(` { "data": { "viewer": { "projectsV2": { "nodes": [ { "title": "MonalisaV2", "id": "MONALISAV2ID", "resourcePath": "/user/MONALISA/projects/2" } ], "pageInfo": { "hasNextPage": false } } } } } `)) } // TODO interactive metadata tests once: 1) we have test utils for Prompter and 2) metadata questions use Prompter