cli/pkg/cmd/repo/create/create_test.go
Mislav Marohnić 5656296ade repo fork: directly fork under the desired name
A new GitHub feature landed where the API client can specify the desired
name of the new fork. This avoids the necessity of subsequently having
to rename the forked repo after the fork operation has created one.

For backwards compatibility, the renaming logic is still here, but
activates only if the resulting repo name is not the desired name.
2022-07-11 13:54:58 +02:00

497 lines
16 KiB
Go

package create
import (
"bytes"
"net/http"
"testing"
"github.com/cli/cli/v2/internal/config"
"github.com/cli/cli/v2/internal/run"
"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/pkg/prompt"
"github.com/google/shlex"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestNewCmdCreate(t *testing.T) {
tests := []struct {
name string
tty bool
cli string
wantsErr bool
errMsg string
wantsOpts CreateOptions
}{
{
name: "no args tty",
tty: true,
cli: "",
wantsOpts: CreateOptions{Interactive: true},
},
{
name: "no args no-tty",
tty: false,
cli: "",
wantsErr: true,
errMsg: "at least one argument required in non-interactive mode",
},
{
name: "new repo from remote",
cli: "NEWREPO --public --clone",
wantsOpts: CreateOptions{
Name: "NEWREPO",
Public: true,
Clone: true},
},
{
name: "no visibility",
tty: true,
cli: "NEWREPO",
wantsErr: true,
errMsg: "`--public`, `--private`, or `--internal` required when not running interactively",
},
{
name: "multiple visibility",
tty: true,
cli: "NEWREPO --public --private",
wantsErr: true,
errMsg: "expected exactly one of `--public`, `--private`, or `--internal`",
},
{
name: "new remote from local",
cli: "--source=/path/to/repo --private",
wantsOpts: CreateOptions{
Private: true,
Source: "/path/to/repo"},
},
{
name: "new remote from local with remote",
cli: "--source=/path/to/repo --public --remote upstream",
wantsOpts: CreateOptions{
Public: true,
Source: "/path/to/repo",
Remote: "upstream",
},
},
{
name: "new remote from local with push",
cli: "--source=/path/to/repo --push --public",
wantsOpts: CreateOptions{
Public: true,
Source: "/path/to/repo",
Push: true,
},
},
{
name: "new remote from local without visibility",
cli: "--source=/path/to/repo --push",
wantsOpts: CreateOptions{
Source: "/path/to/repo",
Push: true,
},
wantsErr: true,
errMsg: "`--public`, `--private`, or `--internal` required when not running interactively",
},
{
name: "source with template",
cli: "--source=/path/to/repo --private --template mytemplate",
wantsErr: true,
errMsg: "the `--source` option is not supported with `--clone`, `--template`, `--license`, or `--gitignore`",
},
{
name: "include all branches without template",
cli: "--source=/path/to/repo --private --include-all-branches",
wantsErr: true,
errMsg: "the `--include-all-branches` option is only supported when using `--template`",
},
{
name: "new remote from template with include all branches",
cli: "template-repo --template https://github.com/OWNER/REPO --public --include-all-branches",
wantsOpts: CreateOptions{
Name: "template-repo",
Public: true,
Template: "https://github.com/OWNER/REPO",
IncludeAllBranches: true,
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
ios, _, _, _ := iostreams.Test()
ios.SetStdinTTY(tt.tty)
ios.SetStdoutTTY(tt.tty)
f := &cmdutil.Factory{
IOStreams: ios,
}
var opts *CreateOptions
cmd := NewCmdCreate(f, func(o *CreateOptions) error {
opts = o
return nil
})
// TODO STUPID HACK
// cobra aggressively adds help to all commands. since we're not running through the root command
// (which manages help when running for real) and since create has a '-h' flag (for homepage),
// cobra blows up when it tried to add a help flag and -h is already in use. This hack adds a
// dummy help flag with a random shorthand to get around this.
cmd.Flags().BoolP("help", "x", false, "")
args, err := shlex.Split(tt.cli)
require.NoError(t, err)
cmd.SetArgs(args)
cmd.SetIn(&bytes.Buffer{})
cmd.SetOut(&bytes.Buffer{})
cmd.SetErr(&bytes.Buffer{})
_, err = cmd.ExecuteC()
if tt.wantsErr {
assert.Error(t, err)
assert.Equal(t, tt.errMsg, err.Error())
return
} else {
require.NoError(t, err)
}
assert.Equal(t, tt.wantsOpts.Interactive, opts.Interactive)
assert.Equal(t, tt.wantsOpts.Source, opts.Source)
assert.Equal(t, tt.wantsOpts.Name, opts.Name)
assert.Equal(t, tt.wantsOpts.Public, opts.Public)
assert.Equal(t, tt.wantsOpts.Internal, opts.Internal)
assert.Equal(t, tt.wantsOpts.Private, opts.Private)
assert.Equal(t, tt.wantsOpts.Clone, opts.Clone)
})
}
}
func Test_createRun(t *testing.T) {
tests := []struct {
name string
tty bool
opts *CreateOptions
httpStubs func(*httpmock.Registry)
askStubs func(*prompt.AskStubber)
execStubs func(*run.CommandStubber)
wantStdout string
wantErr bool
errMsg string
}{
{
name: "interactive create from scratch with gitignore and license",
opts: &CreateOptions{Interactive: true},
tty: true,
wantStdout: "✓ Created repository OWNER/REPO on GitHub\n",
askStubs: func(as *prompt.AskStubber) {
//nolint:staticcheck // SA1019: as.StubOne is deprecated: use StubPrompt
as.StubOne("Create a new repository on GitHub from scratch")
//nolint:staticcheck // SA1019: as.Stub is deprecated: use StubPrompt
as.Stub([]*prompt.QuestionStub{
{Name: "repoName", Value: "REPO"},
{Name: "repoDescription", Value: "my new repo"},
{Name: "repoVisibility", Value: "Private"},
})
//nolint:staticcheck // SA1019: as.Stub is deprecated: use StubPrompt
as.Stub([]*prompt.QuestionStub{
{Name: "addGitIgnore", Value: true}})
//nolint:staticcheck // SA1019: as.Stub is deprecated: use StubPrompt
as.Stub([]*prompt.QuestionStub{
{Name: "chooseGitIgnore", Value: "Go"}})
//nolint:staticcheck // SA1019: as.Stub is deprecated: use StubPrompt
as.Stub([]*prompt.QuestionStub{
{Name: "addLicense", Value: true}})
//nolint:staticcheck // SA1019: as.Stub is deprecated: use StubPrompt
as.Stub([]*prompt.QuestionStub{
{Name: "chooseLicense", Value: "GNU Lesser General Public License v3.0"}})
//nolint:staticcheck // SA1019: as.Stub is deprecated: use StubPrompt
as.Stub([]*prompt.QuestionStub{
{Name: "confirmSubmit", Value: true}})
//nolint:staticcheck // SA1019: as.StubOne is deprecated: use StubPrompt
as.StubOne(true) //clone locally?
},
httpStubs: func(reg *httpmock.Registry) {
reg.Register(
httpmock.REST("GET", "gitignore/templates"),
httpmock.StringResponse(`["Actionscript","Android","AppceleratorTitanium","Autotools","Bancha","C","C++","Go"]`))
reg.Register(
httpmock.REST("GET", "licenses"),
httpmock.StringResponse(`[{"key": "mit","name": "MIT License"},{"key": "lgpl-3.0","name": "GNU Lesser General Public License v3.0"}]`))
reg.Register(
httpmock.REST("POST", "user/repos"),
httpmock.StringResponse(`{"name":"REPO", "owner":{"login": "OWNER"}, "html_url":"https://github.com/OWNER/REPO"}`))
},
execStubs: func(cs *run.CommandStubber) {
cs.Register(`git clone https://github.com/OWNER/REPO.git`, 0, "")
},
},
{
name: "interactive create from scratch but cancel before submit",
opts: &CreateOptions{Interactive: true},
tty: true,
askStubs: func(as *prompt.AskStubber) {
//nolint:staticcheck // SA1019: as.StubOne is deprecated: use StubPrompt
as.StubOne("Create a new repository on GitHub from scratch")
//nolint:staticcheck // SA1019: as.Stub is deprecated: use StubPrompt
as.Stub([]*prompt.QuestionStub{
{Name: "repoName", Value: "REPO"},
{Name: "repoDescription", Value: "my new repo"},
{Name: "repoVisibility", Value: "Private"},
})
//nolint:staticcheck // SA1019: as.Stub is deprecated: use StubPrompt
as.Stub([]*prompt.QuestionStub{
{Name: "addGitIgnore", Value: false}})
//nolint:staticcheck // SA1019: as.Stub is deprecated: use StubPrompt
as.Stub([]*prompt.QuestionStub{
{Name: "addLicense", Value: false}})
//nolint:staticcheck // SA1019: as.Stub is deprecated: use StubPrompt
as.Stub([]*prompt.QuestionStub{
{Name: "confirmSubmit", Value: false}})
},
wantStdout: "",
wantErr: true,
errMsg: "CancelError",
},
{
name: "interactive with existing repository public",
opts: &CreateOptions{Interactive: true},
tty: true,
askStubs: func(as *prompt.AskStubber) {
//nolint:staticcheck // SA1019: as.StubOne is deprecated: use StubPrompt
as.StubOne("Push an existing local repository to GitHub")
//nolint:staticcheck // SA1019: as.StubOne is deprecated: use StubPrompt
as.StubOne(".")
//nolint:staticcheck // SA1019: as.Stub is deprecated: use StubPrompt
as.Stub([]*prompt.QuestionStub{
{Name: "repoName", Value: "REPO"},
{Name: "repoDescription", Value: "my new repo"},
{Name: "repoVisibility", Value: "Private"},
})
//nolint:staticcheck // SA1019: as.StubOne is deprecated: use StubPrompt
as.StubOne(false)
},
httpStubs: func(reg *httpmock.Registry) {
reg.Register(
httpmock.GraphQL(`mutation RepositoryCreate\b`),
httpmock.StringResponse(`
{
"data": {
"createRepository": {
"repository": {
"id": "REPOID",
"name": "REPO",
"owner": {"login":"OWNER"},
"url": "https://github.com/OWNER/REPO"
}
}
}
}`))
},
execStubs: func(cs *run.CommandStubber) {
cs.Register(`git -C . rev-parse --git-dir`, 0, ".git")
cs.Register(`git -C . rev-parse HEAD`, 0, "commithash")
},
wantStdout: "✓ Created repository OWNER/REPO on GitHub\n",
},
{
name: "interactive with existing repository public add remote",
opts: &CreateOptions{Interactive: true},
tty: true,
askStubs: func(as *prompt.AskStubber) {
//nolint:staticcheck // SA1019: as.StubOne is deprecated: use StubPrompt
as.StubOne("Push an existing local repository to GitHub")
//nolint:staticcheck // SA1019: as.StubOne is deprecated: use StubPrompt
as.StubOne(".")
//nolint:staticcheck // SA1019: as.Stub is deprecated: use StubPrompt
as.Stub([]*prompt.QuestionStub{
{Name: "repoName", Value: "REPO"},
{Name: "repoDescription", Value: "my new repo"},
{Name: "repoVisibility", Value: "Private"},
})
//nolint:staticcheck // SA1019: as.StubOne is deprecated: use StubPrompt
as.StubOne(true) //ask for adding a remote
//nolint:staticcheck // SA1019: as.StubOne is deprecated: use StubPrompt
as.StubOne("origin") //ask for remote name
//nolint:staticcheck // SA1019: as.StubOne is deprecated: use StubPrompt
as.StubOne(false) //ask to push to remote
},
httpStubs: func(reg *httpmock.Registry) {
reg.Register(
httpmock.GraphQL(`mutation RepositoryCreate\b`),
httpmock.StringResponse(`
{
"data": {
"createRepository": {
"repository": {
"id": "REPOID",
"name": "REPO",
"owner": {"login":"OWNER"},
"url": "https://github.com/OWNER/REPO"
}
}
}
}`))
},
execStubs: func(cs *run.CommandStubber) {
cs.Register(`git -C . rev-parse --git-dir`, 0, ".git")
cs.Register(`git -C . rev-parse HEAD`, 0, "commithash")
cs.Register(`git -C . remote add origin https://github.com/OWNER/REPO`, 0, "")
},
wantStdout: "✓ Created repository OWNER/REPO on GitHub\n✓ Added remote https://github.com/OWNER/REPO.git\n",
},
{
name: "interactive with existing repository public, add remote, and push",
opts: &CreateOptions{Interactive: true},
tty: true,
askStubs: func(as *prompt.AskStubber) {
//nolint:staticcheck // SA1019: as.StubOne is deprecated: use StubPrompt
as.StubOne("Push an existing local repository to GitHub")
//nolint:staticcheck // SA1019: as.StubOne is deprecated: use StubPrompt
as.StubOne(".")
//nolint:staticcheck // SA1019: as.Stub is deprecated: use StubPrompt
as.Stub([]*prompt.QuestionStub{
{Name: "repoName", Value: "REPO"},
{Name: "repoDescription", Value: "my new repo"},
{Name: "repoVisibility", Value: "Private"},
})
//nolint:staticcheck // SA1019: as.StubOne is deprecated: use StubPrompt
as.StubOne(true) //ask for adding a remote
//nolint:staticcheck // SA1019: as.StubOne is deprecated: use StubPrompt
as.StubOne("origin") //ask for remote name
//nolint:staticcheck // SA1019: as.StubOne is deprecated: use StubPrompt
as.StubOne(true) //ask to push to remote
},
httpStubs: func(reg *httpmock.Registry) {
reg.Register(
httpmock.GraphQL(`mutation RepositoryCreate\b`),
httpmock.StringResponse(`
{
"data": {
"createRepository": {
"repository": {
"id": "REPOID",
"name": "REPO",
"owner": {"login":"OWNER"},
"url": "https://github.com/OWNER/REPO"
}
}
}
}`))
},
execStubs: func(cs *run.CommandStubber) {
cs.Register(`git -C . rev-parse --git-dir`, 0, ".git")
cs.Register(`git -C . rev-parse HEAD`, 0, "commithash")
cs.Register(`git -C . remote add origin https://github.com/OWNER/REPO`, 0, "")
cs.Register(`git -C . push -u origin HEAD`, 0, "")
},
wantStdout: "✓ Created repository OWNER/REPO on GitHub\n✓ Added remote https://github.com/OWNER/REPO.git\n✓ Pushed commits to https://github.com/OWNER/REPO.git\n",
},
{
name: "noninteractive create from scratch",
opts: &CreateOptions{
Interactive: false,
Name: "REPO",
Visibility: "PRIVATE",
},
tty: false,
httpStubs: func(reg *httpmock.Registry) {
reg.Register(
httpmock.GraphQL(`mutation RepositoryCreate\b`),
httpmock.StringResponse(`
{
"data": {
"createRepository": {
"repository": {
"id": "REPOID",
"name": "REPO",
"owner": {"login":"OWNER"},
"url": "https://github.com/OWNER/REPO"
}
}
}
}`))
},
wantStdout: "https://github.com/OWNER/REPO\n",
},
{
name: "noninteractive create from source",
opts: &CreateOptions{
Interactive: false,
Source: ".",
Name: "REPO",
Visibility: "PRIVATE",
},
tty: false,
httpStubs: func(reg *httpmock.Registry) {
reg.Register(
httpmock.GraphQL(`mutation RepositoryCreate\b`),
httpmock.StringResponse(`
{
"data": {
"createRepository": {
"repository": {
"id": "REPOID",
"name": "REPO",
"owner": {"login":"OWNER"},
"url": "https://github.com/OWNER/REPO"
}
}
}
}`))
},
execStubs: func(cs *run.CommandStubber) {
cs.Register(`git -C . rev-parse --git-dir`, 0, ".git")
cs.Register(`git -C . rev-parse HEAD`, 0, "commithash")
cs.Register(`git -C . remote add origin https://github.com/OWNER/REPO`, 0, "")
},
wantStdout: "https://github.com/OWNER/REPO\n",
},
}
for _, tt := range tests {
//nolint:staticcheck // SA1019: prompt.InitAskStubber is deprecated: use NewAskStubber
q, teardown := prompt.InitAskStubber()
defer teardown()
if tt.askStubs != nil {
tt.askStubs(q)
}
reg := &httpmock.Registry{}
if tt.httpStubs != nil {
tt.httpStubs(reg)
}
tt.opts.HttpClient = func() (*http.Client, error) {
return &http.Client{Transport: reg}, nil
}
tt.opts.Config = func() (config.Config, error) {
return config.NewBlankConfig(), nil
}
cs, restoreRun := run.Stub()
defer restoreRun(t)
if tt.execStubs != nil {
tt.execStubs(cs)
}
ios, _, stdout, stderr := iostreams.Test()
ios.SetStdinTTY(tt.tty)
ios.SetStdoutTTY(tt.tty)
tt.opts.IO = ios
t.Run(tt.name, func(t *testing.T) {
defer reg.Verify(t)
err := createRun(tt.opts)
if tt.wantErr {
assert.Error(t, err)
assert.Equal(t, tt.errMsg, err.Error())
return
}
assert.NoError(t, err)
assert.Equal(t, tt.wantStdout, stdout.String())
assert.Equal(t, "", stderr.String())
})
}
}