492 lines
14 KiB
Go
492 lines
14 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`",
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
io, _, _, _ := iostreams.Test()
|
|
io.SetStdinTTY(tt.tty)
|
|
io.SetStdoutTTY(tt.tty)
|
|
|
|
f := &cmdutil.Factory{
|
|
IOStreams: io,
|
|
}
|
|
|
|
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) {
|
|
as.StubOne("Create a new repository on GitHub from scratch")
|
|
as.Stub([]*prompt.QuestionStub{
|
|
{Name: "repoName", Value: "REPO"},
|
|
{Name: "repoDescription", Value: "my new repo"},
|
|
{Name: "repoVisibility", Value: "PRIVATE"},
|
|
})
|
|
as.Stub([]*prompt.QuestionStub{
|
|
{Name: "addGitIgnore", Value: true}})
|
|
as.Stub([]*prompt.QuestionStub{
|
|
{Name: "chooseGitIgnore", Value: "Go"}})
|
|
as.Stub([]*prompt.QuestionStub{
|
|
{Name: "addLicense", Value: true}})
|
|
as.Stub([]*prompt.QuestionStub{
|
|
{Name: "chooseLicense", Value: "GNU Lesser General Public License v3.0"}})
|
|
as.Stub([]*prompt.QuestionStub{
|
|
{Name: "confirmSubmit", Value: true}})
|
|
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) {
|
|
as.StubOne("Create a new repository on GitHub from scratch")
|
|
as.Stub([]*prompt.QuestionStub{
|
|
{Name: "repoName", Value: "REPO"},
|
|
{Name: "repoDescription", Value: "my new repo"},
|
|
{Name: "repoVisibility", Value: "PRIVATE"},
|
|
})
|
|
as.Stub([]*prompt.QuestionStub{
|
|
{Name: "addGitIgnore", Value: false}})
|
|
as.Stub([]*prompt.QuestionStub{
|
|
{Name: "addLicense", Value: false}})
|
|
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) {
|
|
as.StubOne("Push an existing local repository to GitHub")
|
|
as.StubOne(".")
|
|
as.Stub([]*prompt.QuestionStub{
|
|
{Name: "repoName", Value: "REPO"},
|
|
{Name: "repoDescription", Value: "my new repo"},
|
|
{Name: "repoVisibility", Value: "PRIVATE"},
|
|
})
|
|
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) {
|
|
as.StubOne("Push an existing local repository to GitHub")
|
|
as.StubOne(".")
|
|
as.Stub([]*prompt.QuestionStub{
|
|
{Name: "repoName", Value: "REPO"},
|
|
{Name: "repoDescription", Value: "my new repo"},
|
|
{Name: "repoVisibility", Value: "PRIVATE"},
|
|
})
|
|
as.StubOne(true) //ask for adding a remote
|
|
as.StubOne("origin") //ask for remote name
|
|
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) {
|
|
as.StubOne("Push an existing local repository to GitHub")
|
|
as.StubOne(".")
|
|
as.Stub([]*prompt.QuestionStub{
|
|
{Name: "repoName", Value: "REPO"},
|
|
{Name: "repoDescription", Value: "my new repo"},
|
|
{Name: "repoVisibility", Value: "PRIVATE"},
|
|
})
|
|
as.StubOne(true) //ask for adding a remote
|
|
as.StubOne("origin") //ask for remote name
|
|
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 {
|
|
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)
|
|
}
|
|
|
|
io, _, stdout, stderr := iostreams.Test()
|
|
io.SetStdinTTY(tt.tty)
|
|
io.SetStdoutTTY(tt.tty)
|
|
tt.opts.IO = io
|
|
|
|
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())
|
|
})
|
|
}
|
|
}
|
|
|
|
func Test_getModifiedNormalizedName(t *testing.T) {
|
|
// confirmed using GitHub.com/new
|
|
tests := []struct {
|
|
LocalName string
|
|
NormalizedName string
|
|
}{
|
|
{
|
|
LocalName: "cli",
|
|
NormalizedName: "cli",
|
|
},
|
|
{
|
|
LocalName: "cli.git",
|
|
NormalizedName: "cli",
|
|
},
|
|
{
|
|
LocalName: "@-#$^",
|
|
NormalizedName: "---",
|
|
},
|
|
{
|
|
LocalName: "[cli]",
|
|
NormalizedName: "-cli-",
|
|
},
|
|
{
|
|
LocalName: "Hello World, I'm a new repo!",
|
|
NormalizedName: "Hello-World-I-m-a-new-repo-",
|
|
},
|
|
{
|
|
LocalName: " @E3H*(#$#_$-ZVp,n.7lGq*_eMa-(-zAZSJYg!",
|
|
NormalizedName: "-E3H-_--ZVp-n.7lGq-_eMa---zAZSJYg-",
|
|
},
|
|
{
|
|
LocalName: "I'm a crazy .git repo name .git.git .git",
|
|
NormalizedName: "I-m-a-crazy-.git-repo-name-.git.git-",
|
|
},
|
|
}
|
|
for _, tt := range tests {
|
|
output := normalizeRepoName(tt.LocalName)
|
|
assert.Equal(t, tt.NormalizedName, output)
|
|
}
|
|
}
|