package create import ( "bytes" "encoding/json" "io/ioutil" "net/http" "reflect" "strings" "testing" "github.com/MakeNowJust/heredoc" "github.com/cli/cli/context" "github.com/cli/cli/git" "github.com/cli/cli/internal/config" "github.com/cli/cli/internal/ghrepo" "github.com/cli/cli/internal/run" "github.com/cli/cli/pkg/cmdutil" "github.com/cli/cli/pkg/httpmock" "github.com/cli/cli/pkg/iostreams" "github.com/cli/cli/pkg/prompt" "github.com/cli/cli/test" "github.com/google/shlex" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) func eq(t *testing.T, got interface{}, expected interface{}) { t.Helper() if !reflect.DeepEqual(got, expected) { t.Errorf("expected: %v, got: %v", expected, got) } } func runCommand(rt http.RoundTripper, remotes context.Remotes, branch string, isTTY bool, cli string) (*test.CmdOut, error) { return runCommandWithRootDirOverridden(rt, remotes, branch, isTTY, cli, "") } func runCommandWithRootDirOverridden(rt http.RoundTripper, remotes context.Remotes, branch string, isTTY bool, cli string, rootDir string) (*test.CmdOut, error) { io, _, stdout, stderr := iostreams.Test() io.SetStdoutTTY(isTTY) io.SetStdinTTY(isTTY) io.SetStderrTTY(isTTY) factory := &cmdutil.Factory{ IOStreams: io, HttpClient: func() (*http.Client, error) { return &http.Client{Transport: rt}, nil }, Config: func() (config.Config, error) { return config.NewBlankConfig(), nil }, Remotes: func() (context.Remotes, error) { if remotes != nil { return remotes, nil } return context.Remotes{ { Remote: &git.Remote{ Name: "origin", Resolved: "base", }, Repo: ghrepo.New("OWNER", "REPO"), }, }, nil }, Branch: func() (string, error) { return branch, nil }, } cmd := NewCmdCreate(factory, func(opts *CreateOptions) error { opts.RootDirOverride = rootDir return createRun(opts) }) cmd.PersistentFlags().StringP("repo", "R", "", "") argv, err := shlex.Split(cli) if err != nil { return nil, err } cmd.SetArgs(argv) cmd.SetIn(&bytes.Buffer{}) cmd.SetOut(ioutil.Discard) cmd.SetErr(ioutil.Discard) _, err = cmd.ExecuteC() return &test.CmdOut{ OutBuf: stdout, ErrBuf: stderr, }, err } func initFakeHTTP() *httpmock.Registry { return &httpmock.Registry{} } func TestPRCreate_nontty_web(t *testing.T) { http := initFakeHTTP() defer http.Verify(t) http.StubRepoInfoResponse("OWNER", "REPO", "master") cs, cmdTeardown := test.InitCmdStubber() defer cmdTeardown() cs.Stub("") // git status cs.Stub("1234567890,commit 0\n2345678901,commit 1") // git log cs.Stub("") // browser output, err := runCommand(http, nil, "feature", false, `--web --head=feature`) require.NoError(t, err) eq(t, output.String(), "") eq(t, output.Stderr(), "") eq(t, len(cs.Calls), 3) browserCall := cs.Calls[2].Args eq(t, browserCall[len(browserCall)-1], "https://github.com/OWNER/REPO/compare/master...feature?expand=1") } func TestPRCreate_nontty_insufficient_flags(t *testing.T) { http := initFakeHTTP() defer http.Verify(t) output, err := runCommand(http, nil, "feature", false, "") if err == nil { t.Fatal("expected error") } assert.Equal(t, "--title or --fill required when not running interactively", err.Error()) assert.Equal(t, "", output.String()) } func TestPRCreate_nontty(t *testing.T) { http := initFakeHTTP() defer http.Verify(t) http.StubRepoInfoResponse("OWNER", "REPO", "master") http.StubResponse(200, bytes.NewBufferString(` { "data": { "repository": { "pullRequests": { "nodes" : [ ] } } } } `)) http.StubResponse(200, bytes.NewBufferString(` { "data": { "createPullRequest": { "pullRequest": { "URL": "https://github.com/OWNER/REPO/pull/12" } } } } `)) cs, cmdTeardown := test.InitCmdStubber() defer cmdTeardown() cs.Stub("") // git status cs.Stub("1234567890,commit 0\n2345678901,commit 1") // git log output, err := runCommand(http, nil, "feature", false, `-t "my title" -b "my body" -H feature`) require.NoError(t, err) bodyBytes, _ := ioutil.ReadAll(http.Requests[2].Body) reqBody := struct { Variables struct { Input struct { RepositoryID string Title string Body string BaseRefName string HeadRefName string } } }{} _ = json.Unmarshal(bodyBytes, &reqBody) assert.Equal(t, "REPOID", reqBody.Variables.Input.RepositoryID) assert.Equal(t, "my title", reqBody.Variables.Input.Title) assert.Equal(t, "my body", reqBody.Variables.Input.Body) assert.Equal(t, "master", reqBody.Variables.Input.BaseRefName) assert.Equal(t, "feature", reqBody.Variables.Input.HeadRefName) assert.Equal(t, "", output.Stderr()) assert.Equal(t, "https://github.com/OWNER/REPO/pull/12\n", output.String()) } func TestPRCreate(t *testing.T) { http := initFakeHTTP() defer http.Verify(t) http.StubRepoInfoResponse("OWNER", "REPO", "master") http.StubRepoResponse("OWNER", "REPO") http.Register( httpmock.GraphQL(`query UserCurrent\b`), httpmock.StringResponse(`{"data": {"viewer": {"login": "OWNER"} } }`)) http.Register( httpmock.GraphQL(`query PullRequestForBranch\b`), httpmock.StringResponse(` { "data": { "repository": { "pullRequests": { "nodes" : [ ] } } } } `)) http.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)) })) cs, cmdTeardown := test.InitCmdStubber() defer cmdTeardown() cs.Stub("") // git config --get-regexp (determineTrackingBranch) cs.Stub("") // git show-ref --verify (determineTrackingBranch) cs.Stub("") // git status cs.Stub("1234567890,commit 0\n2345678901,commit 1") // git log cs.Stub("") // git push ask, cleanupAsk := prompt.InitAskStubber() defer cleanupAsk() ask.StubOne(0) output, err := runCommand(http, nil, "feature", true, `-t "my title" -b "my body"`) require.NoError(t, err) assert.Equal(t, "https://github.com/OWNER/REPO/pull/12\n", output.String()) assert.Equal(t, "\nCreating pull request for feature into master in OWNER/REPO\n\n", output.Stderr()) } func TestPRCreate_createFork(t *testing.T) { http := initFakeHTTP() defer http.Verify(t) http.StubRepoInfoResponse("OWNER", "REPO", "master") http.StubRepoResponse("OWNER", "REPO") http.Register( httpmock.GraphQL(`query UserCurrent\b`), httpmock.StringResponse(`{"data": {"viewer": {"login": "monalisa"} } }`)) http.Register( httpmock.GraphQL(`query PullRequestForBranch\b`), httpmock.StringResponse(` { "data": { "repository": { "pullRequests": { "nodes" : [ ] } } } } `)) http.Register( httpmock.REST("POST", "repos/OWNER/REPO/forks"), httpmock.StatusStringResponse(201, ` { "node_id": "NODEID", "name": "REPO", "owner": {"login": "monalisa"} } `)) http.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)) })) cs, cmdTeardown := test.InitCmdStubber() defer cmdTeardown() cs.Stub("") // git config --get-regexp (determineTrackingBranch) cs.Stub("") // git show-ref --verify (determineTrackingBranch) cs.Stub("") // git status cs.Stub("1234567890,commit 0\n2345678901,commit 1") // git log cs.Stub("") // git remote add cs.Stub("") // git push ask, cleanupAsk := prompt.InitAskStubber() defer cleanupAsk() ask.StubOne(1) output, err := runCommand(http, nil, "feature", true, `-t title -b body`) require.NoError(t, err) assert.Equal(t, []string{"git", "remote", "add", "-f", "fork", "https://github.com/monalisa/REPO.git"}, cs.Calls[4].Args) assert.Equal(t, []string{"git", "push", "--set-upstream", "fork", "HEAD:feature"}, cs.Calls[5].Args) assert.Equal(t, "https://github.com/OWNER/REPO/pull/12\n", output.String()) } func TestPRCreate_pushedToNonBaseRepo(t *testing.T) { remotes := 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"), }, } http := initFakeHTTP() defer http.Verify(t) http.StubRepoInfoResponse("OWNER", "REPO", "master") http.Register( httpmock.GraphQL(`query PullRequestForBranch\b`), httpmock.StringResponse(` { "data": { "repository": { "pullRequests": { "nodes" : [ ] } } } } `)) http.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)) })) cs, cmdTeardown := run.Stub() defer cmdTeardown(t) cs.Register("git status", 0, "") 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 cs.Register("git .+ log", 1, "", func(args []string) { assert.Equal(t, "upstream/master...feature", args[len(args)-1]) }) _, cleanupAsk := prompt.InitAskStubber() defer cleanupAsk() output, err := runCommand(http, remotes, "feature", true, `-t title -b body`) require.NoError(t, err) assert.Equal(t, "\nCreating pull request for monalisa:feature into master in OWNER/REPO\n\n", output.Stderr()) assert.Equal(t, "https://github.com/OWNER/REPO/pull/12\n", output.String()) } func TestPRCreate_pushedToDifferentBranchName(t *testing.T) { http := initFakeHTTP() defer http.Verify(t) http.StubRepoInfoResponse("OWNER", "REPO", "master") http.Register( httpmock.GraphQL(`query PullRequestForBranch\b`), httpmock.StringResponse(` { "data": { "repository": { "pullRequests": { "nodes" : [ ] } } } } `)) http.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)) })) cs, cmdTeardown := run.Stub() defer cmdTeardown(t) cs.Register("git status", 0, "") 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 cs.Register("git .+ log", 1, "", func(args []string) { assert.Equal(t, "origin/master...feature", args[len(args)-1]) }) _, cleanupAsk := prompt.InitAskStubber() defer cleanupAsk() output, err := runCommand(http, nil, "feature", true, `-t title -b body`) require.NoError(t, err) assert.Equal(t, "\nCreating pull request for my-feat2 into master in OWNER/REPO\n\n", output.Stderr()) assert.Equal(t, "https://github.com/OWNER/REPO/pull/12\n", output.String()) } func TestPRCreate_nonLegacyTemplate(t *testing.T) { http := initFakeHTTP() defer http.Verify(t) http.StubRepoInfoResponse("OWNER", "REPO", "master") http.Register( httpmock.GraphQL(`query PullRequestForBranch\b`), httpmock.StringResponse(` { "data": { "repository": { "pullRequests": { "nodes" : [ ] } } } } `)) http.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\nFixes a bug and Closes an issue", input["body"].(string)) })) cs, cmdTeardown := test.InitCmdStubber() defer cmdTeardown() cs.Stub("") // git status cs.Stub("1234567890,commit 0\n2345678901,commit 1") // git log as, teardown := prompt.InitAskStubber() defer teardown() as.Stub([]*prompt.QuestionStub{ { Name: "index", Value: 0, }, }) as.Stub([]*prompt.QuestionStub{ { Name: "body", Default: true, }, }) as.Stub([]*prompt.QuestionStub{ { Name: "confirmation", Value: 0, }, }) output, err := runCommandWithRootDirOverridden(http, nil, "feature", true, `-t "my title" -H feature`, "./fixtures/repoWithNonLegacyPRTemplates") require.NoError(t, err) eq(t, output.String(), "https://github.com/OWNER/REPO/pull/12\n") } func TestPRCreate_metadata(t *testing.T) { http := initFakeHTTP() defer http.Verify(t) http.StubRepoInfoResponse("OWNER", "REPO", "master") http.Register( httpmock.GraphQL(`query PullRequestForBranch\b`), httpmock.StringResponse(` { "data": { "repository": { "pullRequests": { "nodes": [ ] } } } } `)) http.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" } } } } `)) 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": { "projects": { "nodes": [], "pageInfo": { "hasNextPage": false } } } } } `)) http.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{}) { eq(t, inputs["title"], "TITLE") eq(t, inputs["body"], "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) } })) http.Register( httpmock.GraphQL(`mutation PullRequestCreateMetadata\b`), httpmock.GraphQLMutation(` { "data": { "updatePullRequest": { "clientMutationId": "" } } } `, func(inputs map[string]interface{}) { eq(t, inputs["pullRequestId"], "NEWPULLID") eq(t, inputs["assigneeIds"], []interface{}{"MONAID"}) eq(t, inputs["labelIds"], []interface{}{"BUGID", "TODOID"}) eq(t, inputs["projectIds"], []interface{}{"ROADMAPID"}) eq(t, inputs["milestoneId"], "BIGONEID") })) http.Register( httpmock.GraphQL(`mutation PullRequestCreateRequestReviews\b`), httpmock.GraphQLMutation(` { "data": { "requestReviews": { "clientMutationId": "" } } } `, func(inputs map[string]interface{}) { eq(t, inputs["pullRequestId"], "NEWPULLID") eq(t, inputs["userIds"], []interface{}{"HUBOTID", "MONAID"}) eq(t, inputs["teamIds"], []interface{}{"COREID", "ROBOTID"}) eq(t, inputs["union"], true) })) cs, cmdTeardown := test.InitCmdStubber() defer cmdTeardown() cs.Stub("") // git status cs.Stub("1234567890,commit 0\n2345678901,commit 1") // git log 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`) eq(t, err, nil) eq(t, output.String(), "https://github.com/OWNER/REPO/pull/12\n") } func TestPRCreate_alreadyExists(t *testing.T) { http := initFakeHTTP() defer http.Verify(t) http.StubRepoInfoResponse("OWNER", "REPO", "master") http.StubResponse(200, bytes.NewBufferString(` { "data": { "repository": { "pullRequests": { "nodes": [ { "url": "https://github.com/OWNER/REPO/pull/123", "headRefName": "feature", "baseRefName": "master" } ] } } } } `)) cs, cmdTeardown := test.InitCmdStubber() defer cmdTeardown() cs.Stub("") // git config --get-regexp (determineTrackingBranch) cs.Stub("") // git show-ref --verify (determineTrackingBranch) cs.Stub("") // git status cs.Stub("1234567890,commit 0\n2345678901,commit 1") // git log _, err := runCommand(http, nil, "feature", true, `-H feature`) if err == nil { t.Fatal("error expected, got nil") } if err.Error() != "a pull request for branch \"feature\" into branch \"master\" already exists:\nhttps://github.com/OWNER/REPO/pull/123" { t.Errorf("got error %q", err) } } func TestPRCreate_web(t *testing.T) { http := initFakeHTTP() defer http.Verify(t) http.StubRepoInfoResponse("OWNER", "REPO", "master") http.StubRepoResponse("OWNER", "REPO") http.Register( httpmock.GraphQL(`query UserCurrent\b`), httpmock.StringResponse(`{"data": {"viewer": {"login": "OWNER"} } }`)) cs, cmdTeardown := test.InitCmdStubber() defer cmdTeardown() cs.Stub("") // git config --get-regexp (determineTrackingBranch) cs.Stub("") // git show-ref --verify (determineTrackingBranch) cs.Stub("") // git status cs.Stub("1234567890,commit 0\n2345678901,commit 1") // git log cs.Stub("") // git push cs.Stub("") // browser ask, cleanupAsk := prompt.InitAskStubber() defer cleanupAsk() ask.StubOne(0) output, err := runCommand(http, nil, "feature", true, `--web`) require.NoError(t, err) eq(t, output.String(), "") eq(t, output.Stderr(), "Opening github.com/OWNER/REPO/compare/master...feature in your browser.\n") eq(t, len(cs.Calls), 6) eq(t, strings.Join(cs.Calls[4].Args, " "), "git push --set-upstream origin HEAD:feature") browserCall := cs.Calls[5].Args eq(t, browserCall[len(browserCall)-1], "https://github.com/OWNER/REPO/compare/master...feature?expand=1") } func Test_determineTrackingBranch_empty(t *testing.T) { cs, cmdTeardown := test.InitCmdStubber() defer cmdTeardown() remotes := context.Remotes{} cs.Stub("") // git config --get-regexp (ReadBranchConfig) cs.Stub("deadbeef HEAD") // git show-ref --verify (ShowRefs) ref := determineTrackingBranch(remotes, "feature") if ref != nil { t.Errorf("expected nil result, got %v", ref) } } func Test_determineTrackingBranch_noMatch(t *testing.T) { cs, cmdTeardown := test.InitCmdStubber() defer cmdTeardown() 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"), }, } cs.Stub("") // git config --get-regexp (ReadBranchConfig) cs.Stub(`deadbeef HEAD deadb00f refs/remotes/origin/feature`) // git show-ref --verify (ShowRefs) ref := determineTrackingBranch(remotes, "feature") if ref != nil { t.Errorf("expected nil result, got %v", ref) } } func Test_determineTrackingBranch_hasMatch(t *testing.T) { cs, cmdTeardown := test.InitCmdStubber() defer cmdTeardown() 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"), }, } cs.Stub("") // git config --get-regexp (ReadBranchConfig) cs.Stub(`deadbeef HEAD deadb00f refs/remotes/origin/feature deadbeef refs/remotes/upstream/feature`) // git show-ref --verify (ShowRefs) ref := determineTrackingBranch(remotes, "feature") if ref == nil { t.Fatal("expected result, got nil") } eq(t, cs.Calls[1].Args, []string{"git", "show-ref", "--verify", "--", "HEAD", "refs/remotes/origin/feature", "refs/remotes/upstream/feature"}) eq(t, ref.RemoteName, "upstream") eq(t, ref.BranchName, "feature") } func Test_determineTrackingBranch_respectTrackingConfig(t *testing.T) { cs, cmdTeardown := test.InitCmdStubber() defer cmdTeardown() remotes := context.Remotes{ &context.Remote{ Remote: &git.Remote{Name: "origin"}, Repo: ghrepo.New("hubot", "Spoon-Knife"), }, } cs.Stub(`branch.feature.remote origin branch.feature.merge refs/heads/great-feat`) // git config --get-regexp (ReadBranchConfig) cs.Stub(`deadbeef HEAD deadb00f refs/remotes/origin/feature`) // git show-ref --verify (ShowRefs) ref := determineTrackingBranch(remotes, "feature") if ref != nil { t.Errorf("expected nil result, got %v", ref) } eq(t, cs.Calls[1].Args, []string{"git", "show-ref", "--verify", "--", "HEAD", "refs/remotes/origin/great-feat", "refs/remotes/origin/feature"}) } func Test_generateCompareURL(t *testing.T) { type args struct { r ghrepo.Interface base string head string title string body string assignees []string labels []string projects []string milestones []string } tests := []struct { name string args args want string wantErr bool }{ { name: "basic", args: args{ r: ghrepo.New("OWNER", "REPO"), base: "main", head: "feature", }, want: "https://github.com/OWNER/REPO/compare/main...feature?expand=1", wantErr: false, }, { name: "with labels", args: args{ r: ghrepo.New("OWNER", "REPO"), base: "a", head: "b", labels: []string{"one", "two three"}, }, want: "https://github.com/OWNER/REPO/compare/a...b?expand=1&labels=one%2Ctwo+three", wantErr: false, }, { name: "complex branch names", args: args{ r: ghrepo.New("OWNER", "REPO"), base: "main/trunk", head: "owner:feature", }, want: "https://github.com/OWNER/REPO/compare/main%2Ftrunk...owner%3Afeature?expand=1", wantErr: false, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { got, err := generateCompareURL(tt.args.r, tt.args.base, tt.args.head, tt.args.title, tt.args.body, tt.args.assignees, tt.args.labels, tt.args.projects, tt.args.milestones) 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) } }) } }