cli/pkg/cmd/issue/develop/develop_test.go
2022-10-24 15:02:24 -07:00

606 lines
18 KiB
Go

package develop
import (
"errors"
"net/http"
"testing"
"github.com/cli/cli/v2/context"
"github.com/cli/cli/v2/git"
"github.com/cli/cli/v2/internal/config"
"github.com/cli/cli/v2/internal/ghrepo"
"github.com/cli/cli/v2/internal/run"
"github.com/cli/cli/v2/pkg/httpmock"
"github.com/cli/cli/v2/pkg/iostreams"
"github.com/cli/cli/v2/pkg/prompt"
"github.com/cli/cli/v2/test"
"github.com/stretchr/testify/assert"
)
func Test_developRun(t *testing.T) {
featureEnabledPayload := `{
"data": {
"LinkedBranch": {
"fields": [
{
"name": "id"
},
{
"name": "ref"
}
]
}
}
}`
featureDisabledPayload := `{ "data": { "LinkedBranch": null } }`
tests := []struct {
name string
setup func(*DevelopOptions, *testing.T) func()
cmdStubs func(*run.CommandStubber)
runStubs func(*run.CommandStubber)
remotes map[string]string
askStubs func(*prompt.AskStubber) // TODO eventually migrate to PrompterMock
httpStubs func(*httpmock.Registry, *testing.T)
expectedOut string
expectedErrOut string
expectedBrowse string
wantErr string
tty bool
}{
{name: "list branches for an issue",
setup: func(opts *DevelopOptions, t *testing.T) func() {
opts.IssueSelector = "42"
opts.List = true
return func() {}
},
httpStubs: func(reg *httpmock.Registry, t *testing.T) {
reg.Register(
httpmock.GraphQL(`query LinkedBranch_fields\b`),
httpmock.StringResponse(featureEnabledPayload),
)
reg.Register(
httpmock.GraphQL(`query BranchIssueReferenceListLinkedBranches\b`),
httpmock.GraphQLQuery(`{
"data": {
"repository": {
"issue": {
"linkedBranches": {
"edges": [
{
"node": {
"ref": {
"name": "foo"
}
}
},
{
"node": {
"ref": {
"name": "bar"
}
}
}
]
}
}
}
}
}
`, func(query string, inputs map[string]interface{}) {
assert.Equal(t, float64(42), inputs["issueNumber"])
assert.Equal(t, "OWNER", inputs["repositoryOwner"])
assert.Equal(t, "REPO", inputs["repositoryName"])
}))
},
expectedOut: "foo\nbar\n",
},
{name: "list branches for an issue in tty",
setup: func(opts *DevelopOptions, t *testing.T) func() {
opts.IssueSelector = "42"
opts.List = true
return func() {}
},
tty: true,
httpStubs: func(reg *httpmock.Registry, t *testing.T) {
reg.Register(
httpmock.GraphQL(`query LinkedBranch_fields\b`),
httpmock.StringResponse(featureEnabledPayload),
)
reg.Register(
httpmock.GraphQL(`query BranchIssueReferenceListLinkedBranches\b`),
httpmock.GraphQLQuery(`{
"data": {
"repository": {
"issue": {
"linkedBranches": {
"edges": [
{
"node": {
"ref": {
"name": "foo",
"repository": {
"url": "http://github.localhost/OWNER/REPO"
}
}
}
},
{
"node": {
"ref": {
"name": "bar",
"repository": {
"url": "http://github.localhost/OWNER/OTHER-REPO"
}
}
}
}
]
}
}
}
}
}
`, func(query string, inputs map[string]interface{}) {
assert.Equal(t, float64(42), inputs["issueNumber"])
assert.Equal(t, "OWNER", inputs["repositoryOwner"])
assert.Equal(t, "REPO", inputs["repositoryName"])
}))
},
expectedOut: "\nShowing linked branches for OWNER/REPO#42\n\nfoo http://github.localhost/OWNER/REPO/tree/foo\nbar http://github.localhost/OWNER/OTHER-REPO/tree/bar\n",
},
{name: "list branches for an issue providing an issue url",
setup: func(opts *DevelopOptions, t *testing.T) func() {
opts.IssueSelector = "https://github.com/cli/test-repo/issues/42"
opts.List = true
return func() {}
},
httpStubs: func(reg *httpmock.Registry, t *testing.T) {
reg.Register(
httpmock.GraphQL(`query LinkedBranch_fields\b`),
httpmock.StringResponse(featureEnabledPayload),
)
reg.Register(
httpmock.GraphQL(`query BranchIssueReferenceListLinkedBranches\b`),
httpmock.GraphQLQuery(`{
"data": {
"repository": {
"issue": {
"linkedBranches": {
"edges": [
{
"node": {
"ref": {
"name": "foo"
}
}
},
{
"node": {
"ref": {
"name": "bar"
}
}
}
]
}
}
}
}
}
`, func(query string, inputs map[string]interface{}) {
assert.Equal(t, float64(42), inputs["issueNumber"])
assert.Equal(t, "cli", inputs["repositoryOwner"])
assert.Equal(t, "test-repo", inputs["repositoryName"])
}))
},
expectedOut: "foo\nbar\n",
},
{name: "list branches for an issue providing an issue repo",
setup: func(opts *DevelopOptions, t *testing.T) func() {
opts.IssueSelector = "42"
opts.IssueRepoSelector = "cli/test-repo"
opts.List = true
return func() {}
},
httpStubs: func(reg *httpmock.Registry, t *testing.T) {
reg.Register(
httpmock.GraphQL(`query LinkedBranch_fields\b`),
httpmock.StringResponse(featureEnabledPayload),
)
reg.Register(
httpmock.GraphQL(`query BranchIssueReferenceListLinkedBranches\b`),
httpmock.GraphQLQuery(`{
"data": {
"repository": {
"issue": {
"linkedBranches": {
"edges": [
{
"node": {
"ref": {
"name": "foo"
}
}
},
{
"node": {
"ref": {
"name": "bar"
}
}
}
]
}
}
}
}
}
`, func(query string, inputs map[string]interface{}) {
assert.Equal(t, float64(42), inputs["issueNumber"])
assert.Equal(t, "cli", inputs["repositoryOwner"])
assert.Equal(t, "test-repo", inputs["repositoryName"])
}))
},
expectedOut: "foo\nbar\n",
},
{name: "list branches for an issue providing an issue url and specifying the same repo works",
setup: func(opts *DevelopOptions, t *testing.T) func() {
opts.IssueSelector = "https://github.com/cli/test-repo/issues/42"
opts.IssueRepoSelector = "cli/test-repo"
opts.List = true
return func() {}
},
httpStubs: func(reg *httpmock.Registry, t *testing.T) {
reg.Register(
httpmock.GraphQL(`query LinkedBranch_fields\b`),
httpmock.StringResponse(featureEnabledPayload),
)
reg.Register(
httpmock.GraphQL(`query BranchIssueReferenceListLinkedBranches\b`),
httpmock.GraphQLQuery(`{
"data": {
"repository": {
"issue": {
"linkedBranches": {
"edges": [
{
"node": {
"ref": {
"name": "foo"
}
}
},
{
"node": {
"ref": {
"name": "bar"
}
}
}
]
}
}
}
}
}
`, func(query string, inputs map[string]interface{}) {
assert.Equal(t, float64(42), inputs["issueNumber"])
assert.Equal(t, "cli", inputs["repositoryOwner"])
assert.Equal(t, "test-repo", inputs["repositoryName"])
}))
},
expectedOut: "foo\nbar\n",
},
{name: "list branches for an issue providing an issue url and specifying a different repo returns an error",
setup: func(opts *DevelopOptions, t *testing.T) func() {
opts.IssueSelector = "https://github.com/cli/test-repo/issues/42"
opts.IssueRepoSelector = "cli/other"
opts.List = true
return func() {}
},
httpStubs: func(reg *httpmock.Registry, t *testing.T) {
reg.Register(
httpmock.GraphQL(`query LinkedBranch_fields\b`),
httpmock.StringResponse(featureEnabledPayload),
)
},
wantErr: "issue repo in url cli/test-repo does not match the repo from --issue-repo cli/other",
},
{name: "returns an error when the feature isn't enabled in the GraphQL API",
setup: func(opts *DevelopOptions, t *testing.T) func() {
opts.IssueSelector = "https://github.com/cli/test-repo/issues/42"
opts.List = true
return func() {}
},
httpStubs: func(reg *httpmock.Registry, t *testing.T) {
reg.Register(
httpmock.GraphQL(`query LinkedBranch_fields\b`),
httpmock.StringResponse(featureDisabledPayload),
)
},
wantErr: "the `gh issue develop` command is not currently available",
},
{name: "develop new branch with a name provided",
setup: func(opts *DevelopOptions, t *testing.T) func() {
opts.Name = "my-branch"
opts.BaseBranch = "main"
opts.IssueSelector = "123"
return func() {}
},
httpStubs: func(reg *httpmock.Registry, t *testing.T) {
reg.Register(
httpmock.GraphQL(`query LinkedBranch_fields\b`),
httpmock.StringResponse(featureEnabledPayload),
)
reg.Register(
httpmock.GraphQL(`query RepositoryInfo\b`),
httpmock.StringResponse(`
{ "data": { "repository": {
"id": "REPOID",
"hasIssuesEnabled": true
} } }`),
)
reg.Register(
httpmock.GraphQL(`query IssueByNumber\b`),
httpmock.StringResponse(`{"data":{"repository":{ "hasIssuesEnabled": true, "issue":{"id": "yar", "number":123, "title":"my issue"} }}}`))
reg.Register(
httpmock.GraphQL(`query BranchIssueReferenceFindBaseOid\b`),
httpmock.StringResponse(`{"data":{"repository":{"ref":{"target":{"oid":"123"}}}}}`))
reg.Register(
httpmock.GraphQL(`(?s)mutation CreateLinkedBranch\b.*issueId: \$issueId,\s+name: \$name,\s+oid: \$oid,`),
httpmock.GraphQLQuery(`{ "data": { "createLinkedBranch": { "linkedBranch": {"id": "2", "ref": {"name": "my-branch"} } } } }`,
func(query string, inputs map[string]interface{}) {
assert.Equal(t, "REPOID", inputs["repositoryId"])
assert.Equal(t, "my-branch", inputs["name"])
assert.Equal(t, "yar", inputs["issueId"])
}),
)
},
expectedOut: "github.com/OWNER/REPO/tree/my-branch\n",
},
{name: "develop new branch without a name provided omits the param from the mutation",
setup: func(opts *DevelopOptions, t *testing.T) func() {
opts.Name = ""
opts.BaseBranch = "main"
opts.IssueSelector = "123"
return func() {}
},
httpStubs: func(reg *httpmock.Registry, t *testing.T) {
reg.Register(
httpmock.GraphQL(`query LinkedBranch_fields\b`),
httpmock.StringResponse(featureEnabledPayload),
)
reg.Register(
httpmock.GraphQL(`query RepositoryInfo\b`),
httpmock.StringResponse(`
{ "data": { "repository": {
"id": "REPOID",
"hasIssuesEnabled": true
} } }`),
)
reg.Register(
httpmock.GraphQL(`query IssueByNumber\b`),
httpmock.StringResponse(`{"data":{"repository":{ "hasIssuesEnabled": true, "issue":{"id": "yar", "number":123, "title":"my issue"} }}}`))
reg.Register(
httpmock.GraphQL(`query BranchIssueReferenceFindBaseOid\b`),
httpmock.StringResponse(`{"data":{"repository":{"ref":{"target":{"oid":"123"}}}}}`))
reg.Register(
httpmock.GraphQL(`(?s)mutation CreateLinkedBranch\b.*\$oid: GitObjectID!, \$repositoryId:.*issueId: \$issueId,\s+oid: \$oid,`),
httpmock.GraphQLQuery(`{ "data": { "createLinkedBranch": { "linkedBranch": {"id": "2", "ref": {"name": "my-issue-1"} } } } }`,
func(query string, inputs map[string]interface{}) {
assert.Equal(t, "REPOID", inputs["repositoryId"])
assert.Equal(t, "", inputs["name"])
assert.Equal(t, "yar", inputs["issueId"])
}),
)
},
expectedOut: "github.com/OWNER/REPO/tree/my-issue-1\n",
},
{name: "develop providing an issue url and specifying a different repo returns an error",
setup: func(opts *DevelopOptions, t *testing.T) func() {
opts.IssueSelector = "https://github.com/cli/test-repo/issues/42"
opts.IssueRepoSelector = "cli/other"
return func() {}
},
httpStubs: func(reg *httpmock.Registry, t *testing.T) {
reg.Register(
httpmock.GraphQL(`query LinkedBranch_fields\b`),
httpmock.StringResponse(featureEnabledPayload),
)
reg.Register(
httpmock.GraphQL(`query RepositoryInfo\b`),
httpmock.StringResponse(`
{ "data": { "repository": {
"id": "REPOID",
"hasIssuesEnabled": true
} } }`),
)
},
wantErr: "issue repo in url cli/test-repo does not match the repo from --issue-repo cli/other",
},
{name: "develop new branch with checkout when the branch exists locally",
setup: func(opts *DevelopOptions, t *testing.T) func() {
opts.Name = "my-branch"
opts.BaseBranch = "main"
opts.IssueSelector = "123"
opts.Checkout = true
return func() {}
},
remotes: map[string]string{
"origin": "OWNER/REPO",
},
httpStubs: func(reg *httpmock.Registry, t *testing.T) {
reg.Register(
httpmock.GraphQL(`query LinkedBranch_fields\b`),
httpmock.StringResponse(featureEnabledPayload),
)
reg.Register(
httpmock.GraphQL(`query RepositoryInfo\b`),
httpmock.StringResponse(`
{ "data": { "repository": {
"id": "REPOID",
"hasIssuesEnabled": true
} } }`),
)
reg.Register(
httpmock.GraphQL(`query IssueByNumber\b`),
httpmock.StringResponse(`{"data":{"repository":{ "hasIssuesEnabled": true, "issue":{"id": "yar", "number":123, "title":"my issue"} }}}`))
reg.Register(
httpmock.GraphQL(`query BranchIssueReferenceFindBaseOid\b`),
httpmock.StringResponse(`{"data":{"repository":{"ref":{"target":{"oid":"123"}}}}}`))
reg.Register(
httpmock.GraphQL(`mutation CreateLinkedBranch\b`),
httpmock.GraphQLQuery(`{ "data": { "createLinkedBranch": { "linkedBranch": {"id": "2", "ref": {"name": "my-branch"} } } } }`,
func(query string, inputs map[string]interface{}) {
assert.Equal(t, "REPOID", inputs["repositoryId"])
assert.Equal(t, "my-branch", inputs["name"])
assert.Equal(t, "yar", inputs["issueId"])
}),
)
},
runStubs: func(cs *run.CommandStubber) {
cs.Register(`git rev-parse --verify refs/heads/my-branch`, 0, "")
cs.Register(`git checkout my-branch`, 0, "")
cs.Register(`git pull --ff-only origin my-branch`, 0, "")
},
expectedOut: "github.com/OWNER/REPO/tree/my-branch\n",
},
{name: "develop new branch with checkout when the branch does not exist locally",
setup: func(opts *DevelopOptions, t *testing.T) func() {
opts.Name = "my-branch"
opts.BaseBranch = "main"
opts.IssueSelector = "123"
opts.Checkout = true
return func() {}
},
remotes: map[string]string{
"origin": "OWNER/REPO",
},
httpStubs: func(reg *httpmock.Registry, t *testing.T) {
reg.Register(
httpmock.GraphQL(`query LinkedBranch_fields\b`),
httpmock.StringResponse(featureEnabledPayload),
)
reg.Register(
httpmock.GraphQL(`query RepositoryInfo\b`),
httpmock.StringResponse(`
{ "data": { "repository": {
"id": "REPOID",
"hasIssuesEnabled": true
} } }`),
)
reg.Register(
httpmock.GraphQL(`query IssueByNumber\b`),
httpmock.StringResponse(`{"data":{"repository":{ "hasIssuesEnabled": true, "issue":{"id": "yar", "number":123, "title":"my issue"} }}}`))
reg.Register(
httpmock.GraphQL(`query BranchIssueReferenceFindBaseOid\b`),
httpmock.StringResponse(`{"data":{"repository":{"ref":{"target":{"oid":"123"}}}}}`))
reg.Register(
httpmock.GraphQL(`mutation CreateLinkedBranch\b`),
httpmock.GraphQLQuery(`{ "data": { "createLinkedBranch": { "linkedBranch": {"id": "2", "ref": {"name": "my-branch"} } } } }`,
func(query string, inputs map[string]interface{}) {
assert.Equal(t, "REPOID", inputs["repositoryId"])
assert.Equal(t, "my-branch", inputs["name"])
assert.Equal(t, "yar", inputs["issueId"])
}),
)
},
runStubs: func(cs *run.CommandStubber) {
cs.Register(`git rev-parse --verify refs/heads/my-branch`, 1, "")
cs.Register(`git fetch origin \+refs/heads/my-branch:refs/remotes/origin/my-branch`, 0, "")
cs.Register(`git checkout -b my-branch --track origin/my-branch`, 0, "")
cs.Register(`git pull --ff-only origin my-branch`, 0, "")
},
expectedOut: "github.com/OWNER/REPO/tree/my-branch\n",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
reg := &httpmock.Registry{}
defer reg.Verify(t)
if tt.httpStubs != nil {
tt.httpStubs(reg, t)
}
opts := DevelopOptions{}
ios, _, stdout, stderr := iostreams.Test()
ios.SetStdoutTTY(tt.tty)
ios.SetStdinTTY(tt.tty)
ios.SetStderrTTY(tt.tty)
opts.IO = ios
opts.BaseRepo = func() (ghrepo.Interface, error) {
return ghrepo.New("OWNER", "REPO"), nil
}
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) {
if len(tt.remotes) == 0 {
return nil, errors.New("no remotes")
}
var remotes context.Remotes
for name, repo := range tt.remotes {
r, err := ghrepo.FromFullName(repo)
if err != nil {
return remotes, err
}
remotes = append(remotes, &context.Remote{
Remote: &git.Remote{Name: name},
Repo: r,
})
}
return remotes, nil
}
opts.GitClient = &git.Client{GitPath: "some/path/git"}
cmdStubs, cmdTeardown := run.Stub()
defer cmdTeardown(t)
if tt.runStubs != nil {
tt.runStubs(cmdStubs)
}
cleanSetup := func() {}
if tt.setup != nil {
cleanSetup = tt.setup(&opts, t)
}
defer cleanSetup()
var err error
if opts.List {
err = developRunList(&opts)
} else {
err = developRunCreate(&opts)
}
output := &test.CmdOut{
OutBuf: stdout,
ErrBuf: stderr,
}
if tt.wantErr != "" {
assert.EqualError(t, err, tt.wantErr)
} else {
assert.NoError(t, err)
assert.Equal(t, tt.expectedOut, output.String())
assert.Equal(t, tt.expectedErrOut, output.Stderr())
}
})
}
}