* Remove `Internal` from `gh repo create` prompt when owner is not an org Closes #9464 Internal repos only exist for organizations, so when a user selects their personal namespace to create a repo using `gh repo create`, `Internal` should not be an option in the `Visibility` prompt. This should avoid the additional quirk where if the user selects `Internal` while creating a personal repo and then proceeds to add any of the README, .gitignore, or LICENSE files prompted for later, the repo will not error and instead get created as a `Public` repo. This has the potential for a user to unknowingly leak sensitive info intended to go into a non-public repo. * Refactor prompter with test coverage By extracting the repo visibility options to its own function, getRepoVisibilityOptions, we're able to directly test the behavior introduced with this change. This breaks the testing pattern established here thus far, but may be a good example of the direction we should explore for a future refactor. * Add failing tests to check for error with internal vis in non-org repos There is a bug in the code, currently, where a user repo can attempt to be created as with `--internal` visibility flag when that is not an option for non-org repos. It fails at the API level if the --gitignore, --license, or --add-readme flags are not included, but silently falls back to Public visibility if one of them is included. Because this bug already existed, this commit adds the tests to ensure that both scenarios described above are captured accurately by the test suite. A fix for the latter scenario will be coming in a future commit * Add Exclude to httpmock registry and implement in Test_repoCreate Upon attempting to make the previous commit pass, I realized that it was actually impossible to test what I wanted to. The tests in the previous commit were behaving as expected given the bug that commit described, but upon attempting to implement a solution I realized that the tests were only testing the mocks and not the code functionality itself. Essentially, when the code to fix the bug was implemented, the tests were failing because the mocks required to test the buggy behavior were no longer being called. To make the tests pass, I'd have to rewrite them, but were I to remove the bug fix, the tests would no longer fail. This pointed me to a gap in our httpmocks - the ability to intentionally exclude api calls. The behavior I'm trying to test, here, is that we stop executing when a certain condition is met, and therefore won't make any subsequent api calls down the chain. This implements the Exclude method on the registry such that it will fail if an excluded api pattern is called. I have refactored the tests in Test_repoCreate to use the Exclude mock for testing. * Add error if user attempts to create repo with --internal flag This was previously failing at either the API if no other flags were included or falling back to creating a public repo if one of gitignore, license, or add-readme were included. * Add testing for error messages in gh repo create In the previous commits, we've introduced a new error when a user tries to create an Internal repo not owned by an organization. This adds tests to verify that the error we are getting is, in fact, the one associated with this use case and not some random error.
892 lines
28 KiB
Go
892 lines
28 KiB
Go
package create
|
|
|
|
import (
|
|
"bytes"
|
|
"fmt"
|
|
"net/http"
|
|
"testing"
|
|
|
|
"github.com/cenkalti/backoff/v4"
|
|
"github.com/cli/cli/v2/git"
|
|
"github.com/cli/cli/v2/internal/config"
|
|
"github.com/cli/cli/v2/internal/gh"
|
|
"github.com/cli/cli/v2/internal/prompter"
|
|
"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/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,
|
|
},
|
|
},
|
|
{
|
|
name: "template with .gitignore",
|
|
cli: "template-repo --template mytemplate --gitignore ../.gitignore --public",
|
|
wantsErr: true,
|
|
errMsg: ".gitignore and license templates are not added when template is provided",
|
|
},
|
|
{
|
|
name: "template with license",
|
|
cli: "template-repo --template mytemplate --license ../.license --public",
|
|
wantsErr: true,
|
|
errMsg: ".gitignore and license templates are not added when template is provided",
|
|
},
|
|
}
|
|
|
|
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)
|
|
promptStubs func(*prompter.PrompterMock)
|
|
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 https://github.com/OWNER/REPO\n",
|
|
promptStubs: func(p *prompter.PrompterMock) {
|
|
p.ConfirmFunc = func(message string, defaultValue bool) (bool, error) {
|
|
switch message {
|
|
case "Would you like to add a README file?":
|
|
return false, nil
|
|
case "Would you like to add a .gitignore?":
|
|
return true, nil
|
|
case "Would you like to add a license?":
|
|
return true, nil
|
|
case `This will create "REPO" as a private repository on GitHub. Continue?`:
|
|
return defaultValue, nil
|
|
case "Clone the new repository locally?":
|
|
return defaultValue, nil
|
|
default:
|
|
return false, fmt.Errorf("unexpected confirm prompt: %s", message)
|
|
}
|
|
}
|
|
p.InputFunc = func(message, defaultValue string) (string, error) {
|
|
switch message {
|
|
case "Repository name":
|
|
return "REPO", nil
|
|
case "Description":
|
|
return "my new repo", nil
|
|
default:
|
|
return "", fmt.Errorf("unexpected input prompt: %s", message)
|
|
}
|
|
}
|
|
p.SelectFunc = func(message, defaultValue string, options []string) (int, error) {
|
|
switch message {
|
|
case "What would you like to do?":
|
|
return prompter.IndexFor(options, "Create a new repository on GitHub from scratch")
|
|
case "Visibility":
|
|
return prompter.IndexFor(options, "Private")
|
|
case "Choose a license":
|
|
return prompter.IndexFor(options, "GNU Lesser General Public License v3.0")
|
|
case "Choose a .gitignore template":
|
|
return prompter.IndexFor(options, "Go")
|
|
default:
|
|
return 0, fmt.Errorf("unexpected select prompt: %s", message)
|
|
}
|
|
}
|
|
},
|
|
httpStubs: func(reg *httpmock.Registry) {
|
|
reg.Register(
|
|
httpmock.GraphQL(`query UserCurrent\b`),
|
|
httpmock.StringResponse(`{"data":{"viewer":{"login":"someuser","organizations":{"nodes": []}}}}`))
|
|
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 with prompted owner",
|
|
opts: &CreateOptions{Interactive: true},
|
|
tty: true,
|
|
wantStdout: "✓ Created repository org1/REPO on GitHub\n https://github.com/org1/REPO\n",
|
|
promptStubs: func(p *prompter.PrompterMock) {
|
|
p.ConfirmFunc = func(message string, defaultValue bool) (bool, error) {
|
|
switch message {
|
|
case "Would you like to add a README file?":
|
|
return false, nil
|
|
case "Would you like to add a .gitignore?":
|
|
return false, nil
|
|
case "Would you like to add a license?":
|
|
return false, nil
|
|
case `This will create "org1/REPO" as a private repository on GitHub. Continue?`:
|
|
return true, nil
|
|
case "Clone the new repository locally?":
|
|
return false, nil
|
|
default:
|
|
return false, fmt.Errorf("unexpected confirm prompt: %s", message)
|
|
}
|
|
}
|
|
p.InputFunc = func(message, defaultValue string) (string, error) {
|
|
switch message {
|
|
case "Repository name":
|
|
return "REPO", nil
|
|
case "Description":
|
|
return "my new repo", nil
|
|
default:
|
|
return "", fmt.Errorf("unexpected input prompt: %s", message)
|
|
}
|
|
}
|
|
p.SelectFunc = func(message, defaultValue string, options []string) (int, error) {
|
|
switch message {
|
|
case "Repository owner":
|
|
return prompter.IndexFor(options, "org1")
|
|
case "What would you like to do?":
|
|
return prompter.IndexFor(options, "Create a new repository on GitHub from scratch")
|
|
case "Visibility":
|
|
return prompter.IndexFor(options, "Private")
|
|
default:
|
|
return 0, fmt.Errorf("unexpected select prompt: %s", message)
|
|
}
|
|
}
|
|
},
|
|
httpStubs: func(reg *httpmock.Registry) {
|
|
reg.Register(
|
|
httpmock.GraphQL(`query UserCurrent\b`),
|
|
httpmock.StringResponse(`{"data":{"viewer":{"login":"someuser","organizations":{"nodes": [{"login": "org1"}, {"login": "org2"}]}}}}`))
|
|
reg.Register(
|
|
httpmock.REST("GET", "users/org1"),
|
|
httpmock.StringResponse(`{"login":"org1","type":"Organization"}`))
|
|
reg.Register(
|
|
httpmock.GraphQL(`mutation RepositoryCreate\b`),
|
|
httpmock.StringResponse(`
|
|
{
|
|
"data": {
|
|
"createRepository": {
|
|
"repository": {
|
|
"id": "REPOID",
|
|
"name": "REPO",
|
|
"owner": {"login":"org1"},
|
|
"url": "https://github.com/org1/REPO"
|
|
}
|
|
}
|
|
}
|
|
}`))
|
|
},
|
|
},
|
|
{
|
|
name: "interactive create from scratch but cancel before submit",
|
|
opts: &CreateOptions{Interactive: true},
|
|
tty: true,
|
|
promptStubs: func(p *prompter.PrompterMock) {
|
|
p.ConfirmFunc = func(message string, defaultValue bool) (bool, error) {
|
|
switch message {
|
|
case "Would you like to add a README file?":
|
|
return false, nil
|
|
case "Would you like to add a .gitignore?":
|
|
return false, nil
|
|
case "Would you like to add a license?":
|
|
return false, nil
|
|
case `This will create "REPO" as a private repository on GitHub. Continue?`:
|
|
return false, nil
|
|
default:
|
|
return false, fmt.Errorf("unexpected confirm prompt: %s", message)
|
|
}
|
|
}
|
|
p.InputFunc = func(message, defaultValue string) (string, error) {
|
|
switch message {
|
|
case "Repository name":
|
|
return "REPO", nil
|
|
case "Description":
|
|
return "my new repo", nil
|
|
default:
|
|
return "", fmt.Errorf("unexpected input prompt: %s", message)
|
|
}
|
|
}
|
|
p.SelectFunc = func(message, defaultValue string, options []string) (int, error) {
|
|
switch message {
|
|
case "What would you like to do?":
|
|
return prompter.IndexFor(options, "Create a new repository on GitHub from scratch")
|
|
case "Visibility":
|
|
return prompter.IndexFor(options, "Private")
|
|
default:
|
|
return 0, fmt.Errorf("unexpected select prompt: %s", message)
|
|
}
|
|
}
|
|
},
|
|
httpStubs: func(reg *httpmock.Registry) {
|
|
reg.Register(
|
|
httpmock.GraphQL(`query UserCurrent\b`),
|
|
httpmock.StringResponse(`{"data":{"viewer":{"login":"someuser","organizations":{"nodes": []}}}}`))
|
|
},
|
|
wantStdout: "",
|
|
wantErr: true,
|
|
errMsg: "CancelError",
|
|
},
|
|
{
|
|
name: "interactive with existing repository public",
|
|
opts: &CreateOptions{Interactive: true},
|
|
tty: true,
|
|
promptStubs: func(p *prompter.PrompterMock) {
|
|
p.ConfirmFunc = func(message string, defaultValue bool) (bool, error) {
|
|
switch message {
|
|
case "Add a remote?":
|
|
return false, nil
|
|
default:
|
|
return false, fmt.Errorf("unexpected confirm prompt: %s", message)
|
|
}
|
|
}
|
|
p.InputFunc = func(message, defaultValue string) (string, error) {
|
|
switch message {
|
|
case "Path to local repository":
|
|
return defaultValue, nil
|
|
case "Repository name":
|
|
return "REPO", nil
|
|
case "Description":
|
|
return "my new repo", nil
|
|
default:
|
|
return "", fmt.Errorf("unexpected input prompt: %s", message)
|
|
}
|
|
}
|
|
p.SelectFunc = func(message, defaultValue string, options []string) (int, error) {
|
|
switch message {
|
|
case "What would you like to do?":
|
|
return prompter.IndexFor(options, "Push an existing local repository to GitHub")
|
|
case "Visibility":
|
|
return prompter.IndexFor(options, "Private")
|
|
default:
|
|
return 0, fmt.Errorf("unexpected select prompt: %s", message)
|
|
}
|
|
}
|
|
},
|
|
httpStubs: func(reg *httpmock.Registry) {
|
|
reg.Register(
|
|
httpmock.GraphQL(`query UserCurrent\b`),
|
|
httpmock.StringResponse(`{"data":{"viewer":{"login":"someuser","organizations":{"nodes": []}}}}`))
|
|
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 https://github.com/OWNER/REPO\n",
|
|
},
|
|
{
|
|
name: "interactive with existing repository public add remote and push",
|
|
opts: &CreateOptions{Interactive: true},
|
|
tty: true,
|
|
promptStubs: func(p *prompter.PrompterMock) {
|
|
p.ConfirmFunc = func(message string, defaultValue bool) (bool, error) {
|
|
switch message {
|
|
case "Add a remote?":
|
|
return true, nil
|
|
case `Would you like to push commits from the current branch to "origin"?`:
|
|
return true, nil
|
|
default:
|
|
return false, fmt.Errorf("unexpected confirm prompt: %s", message)
|
|
}
|
|
}
|
|
p.InputFunc = func(message, defaultValue string) (string, error) {
|
|
switch message {
|
|
case "Path to local repository":
|
|
return defaultValue, nil
|
|
case "Repository name":
|
|
return "REPO", nil
|
|
case "Description":
|
|
return "my new repo", nil
|
|
case "What should the new remote be called?":
|
|
return defaultValue, nil
|
|
default:
|
|
return "", fmt.Errorf("unexpected input prompt: %s", message)
|
|
}
|
|
}
|
|
p.SelectFunc = func(message, defaultValue string, options []string) (int, error) {
|
|
switch message {
|
|
case "What would you like to do?":
|
|
return prompter.IndexFor(options, "Push an existing local repository to GitHub")
|
|
case "Visibility":
|
|
return prompter.IndexFor(options, "Private")
|
|
default:
|
|
return 0, fmt.Errorf("unexpected select prompt: %s", message)
|
|
}
|
|
}
|
|
},
|
|
httpStubs: func(reg *httpmock.Registry) {
|
|
reg.Register(
|
|
httpmock.GraphQL(`query UserCurrent\b`),
|
|
httpmock.StringResponse(`{"data":{"viewer":{"login":"someuser","organizations":{"nodes": []}}}}`))
|
|
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 --set-upstream origin HEAD`, 0, "")
|
|
},
|
|
wantStdout: "✓ Created repository OWNER/REPO on GitHub\n https://github.com/OWNER/REPO\n✓ Added remote https://github.com/OWNER/REPO.git\n✓ Pushed commits to https://github.com/OWNER/REPO.git\n",
|
|
},
|
|
{
|
|
name: "interactive create from a template repository",
|
|
opts: &CreateOptions{Interactive: true},
|
|
tty: true,
|
|
promptStubs: func(p *prompter.PrompterMock) {
|
|
p.ConfirmFunc = func(message string, defaultValue bool) (bool, error) {
|
|
switch message {
|
|
case `This will create "OWNER/REPO" as a private repository on GitHub. Continue?`:
|
|
return defaultValue, nil
|
|
case "Clone the new repository locally?":
|
|
return defaultValue, nil
|
|
default:
|
|
return false, fmt.Errorf("unexpected confirm prompt: %s", message)
|
|
}
|
|
}
|
|
p.InputFunc = func(message, defaultValue string) (string, error) {
|
|
switch message {
|
|
case "Repository name":
|
|
return "REPO", nil
|
|
case "Description":
|
|
return "my new repo", nil
|
|
default:
|
|
return "", fmt.Errorf("unexpected input prompt: %s", message)
|
|
}
|
|
}
|
|
p.SelectFunc = func(message, defaultValue string, options []string) (int, error) {
|
|
switch message {
|
|
case "Repository owner":
|
|
return prompter.IndexFor(options, "OWNER")
|
|
case "Choose a template repository":
|
|
return prompter.IndexFor(options, "REPO")
|
|
case "What would you like to do?":
|
|
return prompter.IndexFor(options, "Create a new repository on GitHub from a template repository")
|
|
case "Visibility":
|
|
return prompter.IndexFor(options, "Private")
|
|
default:
|
|
return 0, fmt.Errorf("unexpected select prompt: %s", message)
|
|
}
|
|
}
|
|
},
|
|
httpStubs: func(reg *httpmock.Registry) {
|
|
reg.Register(
|
|
httpmock.GraphQL(`query UserCurrent\b`),
|
|
httpmock.StringResponse(`{"data":{"viewer":{"login":"OWNER","organizations":{"nodes": []}}}}`))
|
|
reg.Register(
|
|
httpmock.GraphQL(`query UserCurrent\b`),
|
|
httpmock.StringResponse(`{"data":{"viewer":{"login":"OWNER","organizations":{"nodes": []}}}}`))
|
|
reg.Register(
|
|
httpmock.GraphQL(`query RepositoryList\b`),
|
|
httpmock.FileResponse("./fixtures/repoTempList.json"))
|
|
reg.Register(
|
|
httpmock.REST("GET", "users/OWNER"),
|
|
httpmock.StringResponse(`{"login":"OWNER","type":"User"}`))
|
|
reg.Register(
|
|
httpmock.GraphQL(`query UserCurrent\b`),
|
|
httpmock.StringResponse(`{"data":{"viewer":{"id":"OWNER"}}}`))
|
|
reg.Register(
|
|
httpmock.GraphQL(`mutation CloneTemplateRepository\b`),
|
|
httpmock.StringResponse(`
|
|
{
|
|
"data": {
|
|
"cloneTemplateRepository": {
|
|
"repository": {
|
|
"id": "REPOID",
|
|
"name": "REPO",
|
|
"owner": {"login":"OWNER"},
|
|
"url": "https://github.com/OWNER/REPO"
|
|
}
|
|
}
|
|
}
|
|
}`))
|
|
},
|
|
execStubs: func(cs *run.CommandStubber) {
|
|
cs.Register(`git clone --branch main https://github.com/OWNER/REPO`, 0, "")
|
|
},
|
|
wantStdout: "✓ Created repository OWNER/REPO on GitHub\n https://github.com/OWNER/REPO\n",
|
|
},
|
|
{
|
|
name: "interactive create from template repo but there are no template repos",
|
|
opts: &CreateOptions{Interactive: true},
|
|
tty: true,
|
|
promptStubs: func(p *prompter.PrompterMock) {
|
|
p.ConfirmFunc = func(message string, defaultValue bool) (bool, error) {
|
|
switch message {
|
|
default:
|
|
return false, fmt.Errorf("unexpected confirm prompt: %s", message)
|
|
}
|
|
}
|
|
p.InputFunc = func(message, defaultValue string) (string, error) {
|
|
switch message {
|
|
case "Repository name":
|
|
return "REPO", nil
|
|
case "Description":
|
|
return "my new repo", nil
|
|
default:
|
|
return "", fmt.Errorf("unexpected input prompt: %s", message)
|
|
}
|
|
}
|
|
p.SelectFunc = func(message, defaultValue string, options []string) (int, error) {
|
|
switch message {
|
|
case "What would you like to do?":
|
|
return prompter.IndexFor(options, "Create a new repository on GitHub from a template repository")
|
|
case "Visibility":
|
|
return prompter.IndexFor(options, "Private")
|
|
default:
|
|
return 0, fmt.Errorf("unexpected select prompt: %s", message)
|
|
}
|
|
}
|
|
},
|
|
httpStubs: func(reg *httpmock.Registry) {
|
|
reg.Register(
|
|
httpmock.GraphQL(`query UserCurrent\b`),
|
|
httpmock.StringResponse(`{"data":{"viewer":{"login":"OWNER","organizations":{"nodes": []}}}}`))
|
|
reg.Register(
|
|
httpmock.GraphQL(`query UserCurrent\b`),
|
|
httpmock.StringResponse(`{"data":{"viewer":{"login":"OWNER","organizations":{"nodes": []}}}}`))
|
|
reg.Register(
|
|
httpmock.GraphQL(`query RepositoryList\b`),
|
|
httpmock.StringResponse(`{"data":{"repositoryOwner":{"login":"OWNER","repositories":{"nodes":[]},"totalCount":0,"pageInfo":{"hasNextPage":false,"endCursor":""}}}}`))
|
|
},
|
|
execStubs: func(cs *run.CommandStubber) {},
|
|
wantStdout: "",
|
|
wantErr: true,
|
|
errMsg: "OWNER has no template repositories",
|
|
},
|
|
{
|
|
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",
|
|
},
|
|
{
|
|
name: "noninteractive clone from scratch",
|
|
opts: &CreateOptions{
|
|
Interactive: false,
|
|
Name: "REPO",
|
|
Visibility: "PRIVATE",
|
|
Clone: true,
|
|
},
|
|
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 init REPO`, 0, "")
|
|
cs.Register(`git -C REPO remote add origin https://github.com/OWNER/REPO`, 0, "")
|
|
},
|
|
wantStdout: "https://github.com/OWNER/REPO\n",
|
|
},
|
|
{
|
|
name: "noninteractive clone with readme",
|
|
opts: &CreateOptions{
|
|
Interactive: false,
|
|
Name: "ElliotAlderson",
|
|
Visibility: "PRIVATE",
|
|
Clone: true,
|
|
AddReadme: true,
|
|
},
|
|
tty: false,
|
|
httpStubs: func(reg *httpmock.Registry) {
|
|
reg.Register(
|
|
httpmock.REST("POST", "user/repos"),
|
|
httpmock.RESTPayload(200, "{\"name\":\"ElliotAlderson\", \"owner\":{\"login\": \"OWNER\"}, \"html_url\":\"https://github.com/OWNER/ElliotAlderson\"}",
|
|
func(payload map[string]interface{}) {
|
|
payload["name"] = "ElliotAlderson"
|
|
payload["owner"] = map[string]interface{}{"login": "OWNER"}
|
|
payload["auto_init"] = true
|
|
payload["private"] = true
|
|
},
|
|
),
|
|
)
|
|
},
|
|
execStubs: func(cs *run.CommandStubber) {
|
|
cs.Register(`git clone https://github.com/OWNER/ElliotAlderson`, 128, "")
|
|
cs.Register(`git clone https://github.com/OWNER/ElliotAlderson`, 0, "")
|
|
},
|
|
wantStdout: "https://github.com/OWNER/ElliotAlderson\n",
|
|
},
|
|
{
|
|
name: "noninteractive create from template with retry",
|
|
opts: &CreateOptions{
|
|
Interactive: false,
|
|
Name: "REPO",
|
|
Visibility: "PRIVATE",
|
|
Clone: true,
|
|
Template: "mytemplate",
|
|
BackOff: &backoff.ZeroBackOff{},
|
|
},
|
|
tty: false,
|
|
httpStubs: func(reg *httpmock.Registry) {
|
|
// Test resolving repo owner from repo name only.
|
|
reg.Register(
|
|
httpmock.GraphQL(`query UserCurrent\b`),
|
|
httpmock.StringResponse(`{"data":{"viewer":{"login":"OWNER"}}}`))
|
|
reg.Register(
|
|
httpmock.GraphQL(`query RepositoryInfo\b`),
|
|
httpmock.GraphQLQuery(`{
|
|
"data": {
|
|
"repository": {
|
|
"id": "REPOID",
|
|
"defaultBranchRef": {
|
|
"name": "main"
|
|
}
|
|
}
|
|
}
|
|
}`, func(s string, m map[string]interface{}) {
|
|
assert.Equal(t, "OWNER", m["owner"])
|
|
assert.Equal(t, "mytemplate", m["name"])
|
|
}),
|
|
)
|
|
reg.Register(
|
|
httpmock.GraphQL(`query UserCurrent\b`),
|
|
httpmock.StringResponse(`{"data":{"viewer":{"id":"OWNERID"}}}`))
|
|
reg.Register(
|
|
httpmock.GraphQL(`mutation CloneTemplateRepository\b`),
|
|
httpmock.GraphQLMutation(`
|
|
{
|
|
"data": {
|
|
"cloneTemplateRepository": {
|
|
"repository": {
|
|
"id": "REPOID",
|
|
"name": "REPO",
|
|
"owner": {"login":"OWNER"},
|
|
"url": "https://github.com/OWNER/REPO"
|
|
}
|
|
}
|
|
}
|
|
}`, func(m map[string]interface{}) {
|
|
assert.Equal(t, "REPOID", m["repositoryId"])
|
|
}))
|
|
},
|
|
execStubs: func(cs *run.CommandStubber) {
|
|
// fatal: Remote branch main not found in upstream origin
|
|
cs.Register(`git clone --branch main https://github.com/OWNER/REPO`, 128, "")
|
|
cs.Register(`git clone --branch main https://github.com/OWNER/REPO`, 0, "")
|
|
},
|
|
wantStdout: "https://github.com/OWNER/REPO\n",
|
|
},
|
|
}
|
|
for _, tt := range tests {
|
|
prompterMock := &prompter.PrompterMock{}
|
|
tt.opts.Prompter = prompterMock
|
|
if tt.promptStubs != nil {
|
|
tt.promptStubs(prompterMock)
|
|
}
|
|
|
|
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() (gh.Config, error) {
|
|
return config.NewBlankConfig(), nil
|
|
}
|
|
|
|
tt.opts.GitClient = &git.Client{
|
|
GhPath: "some/path/gh",
|
|
GitPath: "some/path/git",
|
|
}
|
|
|
|
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) {
|
|
cs, restoreRun := run.Stub()
|
|
defer restoreRun(t)
|
|
if tt.execStubs != nil {
|
|
tt.execStubs(cs)
|
|
}
|
|
|
|
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_getRepoVisibilityOptions(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
owner string
|
|
want []string
|
|
}{
|
|
{
|
|
name: "user repo",
|
|
owner: "",
|
|
want: []string{"Public", "Private"},
|
|
},
|
|
{
|
|
name: "org repo",
|
|
owner: "fooOrg",
|
|
want: []string{"Public", "Private", "Internal"},
|
|
},
|
|
}
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
assert.Equal(t, tt.want, getRepoVisibilityOptions(tt.owner))
|
|
})
|
|
}
|
|
}
|