cli/pkg/cmd/repo/create/http_test.go
Tyler McGoffin 91eb34011c
Remove Internal from gh repo create prompt when owner is not an org (#9465)
* 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.
2024-08-22 10:01:16 -07:00

750 lines
22 KiB
Go

package create
import (
"net/http"
"testing"
"github.com/cli/cli/v2/internal/ghrepo"
"github.com/cli/cli/v2/pkg/httpmock"
"github.com/stretchr/testify/assert"
)
func Test_repoCreate(t *testing.T) {
tests := []struct {
name string
hostname string
input repoCreateInput
stubs func(t *testing.T, r *httpmock.Registry)
wantErr bool
errMsg string
wantRepo string
}{
{
name: "create personal repository",
hostname: "github.com",
input: repoCreateInput{
Name: "winter-foods",
Description: "roasted chestnuts",
HomepageURL: "http://example.com",
Visibility: "public",
HasIssuesEnabled: true,
HasWikiEnabled: true,
},
stubs: func(t *testing.T, r *httpmock.Registry) {
r.Register(
httpmock.GraphQL(`mutation RepositoryCreate\b`),
httpmock.GraphQLMutation(
`{
"data": {
"createRepository": {
"repository": {
"id": "REPOID",
"name": "REPO",
"owner": {"login":"OWNER"},
"url": "the://URL"
}
}
}
}`,
func(inputs map[string]interface{}) {
assert.Equal(t, map[string]interface{}{
"name": "winter-foods",
"description": "roasted chestnuts",
"homepageUrl": "http://example.com",
"visibility": "PUBLIC",
"hasIssuesEnabled": true,
"hasWikiEnabled": true,
}, inputs)
}),
)
},
wantRepo: "https://github.com/OWNER/REPO",
},
{
name: "create Enterprise repository",
hostname: "example.com",
input: repoCreateInput{
Name: "winter-foods",
Description: "roasted chestnuts",
HomepageURL: "http://example.com",
Visibility: "public",
HasIssuesEnabled: true,
HasWikiEnabled: true,
},
stubs: func(t *testing.T, r *httpmock.Registry) {
r.Register(
httpmock.GraphQL(`mutation RepositoryCreate\b`),
httpmock.GraphQLMutation(
`{
"data": {
"createRepository": {
"repository": {
"id": "REPOID",
"name": "REPO",
"owner": {"login":"OWNER"},
"url": "the://URL"
}
}
}
}`,
func(inputs map[string]interface{}) {
assert.Equal(t, map[string]interface{}{
"name": "winter-foods",
"description": "roasted chestnuts",
"homepageUrl": "http://example.com",
"visibility": "PUBLIC",
"hasIssuesEnabled": true,
"hasWikiEnabled": true,
}, inputs)
}),
)
},
wantRepo: "https://example.com/OWNER/REPO",
},
{
name: "create in organization",
hostname: "github.com",
input: repoCreateInput{
Name: "crisps",
Visibility: "internal",
OwnerLogin: "snacks-inc",
HasIssuesEnabled: true,
HasWikiEnabled: true,
},
stubs: func(t *testing.T, r *httpmock.Registry) {
r.Register(
httpmock.REST("GET", "users/snacks-inc"),
httpmock.StringResponse(`{ "node_id": "ORGID", "type": "Organization" }`))
r.Register(
httpmock.GraphQL(`mutation RepositoryCreate\b`),
httpmock.GraphQLMutation(
`{
"data": {
"createRepository": {
"repository": {
"id": "REPOID",
"name": "REPO",
"owner": {"login":"OWNER"},
"url": "the://URL"
}
}
}
}`,
func(inputs map[string]interface{}) {
assert.Equal(t, map[string]interface{}{
"name": "crisps",
"visibility": "INTERNAL",
"ownerId": "ORGID",
"hasIssuesEnabled": true,
"hasWikiEnabled": true,
}, inputs)
}),
)
},
wantRepo: "https://github.com/OWNER/REPO",
},
{
name: "create for team",
hostname: "github.com",
input: repoCreateInput{
Name: "crisps",
Visibility: "internal",
OwnerLogin: "snacks-inc",
TeamSlug: "munchies",
HasIssuesEnabled: true,
HasWikiEnabled: true,
},
stubs: func(t *testing.T, r *httpmock.Registry) {
r.Register(
httpmock.REST("GET", "orgs/snacks-inc/teams/munchies"),
httpmock.StringResponse(`{ "node_id": "TEAMID", "id": 1234, "organization": {"node_id": "ORGID"} }`))
r.Register(
httpmock.GraphQL(`mutation RepositoryCreate\b`),
httpmock.GraphQLMutation(
`{
"data": {
"createRepository": {
"repository": {
"id": "REPOID",
"name": "REPO",
"owner": {"login":"OWNER"},
"url": "the://URL"
}
}
}
}`,
func(inputs map[string]interface{}) {
assert.Equal(t, map[string]interface{}{
"name": "crisps",
"visibility": "INTERNAL",
"ownerId": "ORGID",
"teamId": "TEAMID",
"hasIssuesEnabled": true,
"hasWikiEnabled": true,
}, inputs)
}),
)
},
wantRepo: "https://github.com/OWNER/REPO",
},
{
name: "create personal repo from template repo",
hostname: "github.com",
input: repoCreateInput{
Name: "gen-project",
Description: "my generated project",
Visibility: "private",
TemplateRepositoryID: "TPLID",
HasIssuesEnabled: true,
HasWikiEnabled: true,
IncludeAllBranches: false,
},
stubs: func(t *testing.T, r *httpmock.Registry) {
r.Register(
httpmock.GraphQL(`query UserCurrent\b`),
httpmock.StringResponse(`{"data": {"viewer": {"id":"USERID"} } }`))
r.Register(
httpmock.GraphQL(`mutation CloneTemplateRepository\b`),
httpmock.GraphQLMutation(
`{
"data": {
"cloneTemplateRepository": {
"repository": {
"id": "REPOID",
"name": "REPO",
"owner": {"login":"OWNER"},
"url": "the://URL"
}
}
}
}`,
func(inputs map[string]interface{}) {
assert.Equal(t, map[string]interface{}{
"name": "gen-project",
"description": "my generated project",
"visibility": "PRIVATE",
"ownerId": "USERID",
"repositoryId": "TPLID",
"includeAllBranches": false,
}, inputs)
}),
)
},
wantRepo: "https://github.com/OWNER/REPO",
},
{
name: "create personal repo from template repo, and disable wiki",
hostname: "github.com",
input: repoCreateInput{
Name: "gen-project",
Description: "my generated project",
Visibility: "private",
TemplateRepositoryID: "TPLID",
HasIssuesEnabled: true,
HasWikiEnabled: false,
IncludeAllBranches: false,
},
stubs: func(t *testing.T, r *httpmock.Registry) {
r.Register(
httpmock.GraphQL(`query UserCurrent\b`),
httpmock.StringResponse(`{"data": {"viewer": {"id":"USERID"} } }`))
r.Register(
httpmock.GraphQL(`mutation CloneTemplateRepository\b`),
httpmock.GraphQLMutation(
`{
"data": {
"cloneTemplateRepository": {
"repository": {
"id": "REPOID",
"name": "REPO",
"owner": {"login":"OWNER"},
"url": "the://URL"
}
}
}
}`,
func(inputs map[string]interface{}) {
assert.Equal(t, map[string]interface{}{
"name": "gen-project",
"description": "my generated project",
"visibility": "PRIVATE",
"ownerId": "USERID",
"repositoryId": "TPLID",
"includeAllBranches": false,
}, inputs)
}),
)
r.Register(
httpmock.GraphQL(`mutation UpdateRepository\b`),
httpmock.GraphQLMutation(
`{
"data": {
"updateRepository": {
"repository": {
"id": "REPOID"
}
}
}
}`,
func(inputs map[string]interface{}) {
assert.Equal(t, map[string]interface{}{
"repositoryId": "REPOID",
"hasIssuesEnabled": true,
"hasWikiEnabled": false,
}, inputs)
}),
)
},
wantRepo: "https://github.com/OWNER/REPO",
},
{
name: "create personal repo from template repo, and disable issues",
hostname: "github.com",
input: repoCreateInput{
Name: "gen-project",
Description: "my generated project",
Visibility: "private",
TemplateRepositoryID: "TPLID",
HasIssuesEnabled: false,
HasWikiEnabled: true,
IncludeAllBranches: false,
},
stubs: func(t *testing.T, r *httpmock.Registry) {
r.Register(
httpmock.GraphQL(`query UserCurrent\b`),
httpmock.StringResponse(`{"data": {"viewer": {"id":"USERID"} } }`))
r.Register(
httpmock.GraphQL(`mutation CloneTemplateRepository\b`),
httpmock.GraphQLMutation(
`{
"data": {
"cloneTemplateRepository": {
"repository": {
"id": "REPOID",
"name": "REPO",
"owner": {"login":"OWNER"},
"url": "the://URL"
}
}
}
}`,
func(inputs map[string]interface{}) {
assert.Equal(t, map[string]interface{}{
"name": "gen-project",
"description": "my generated project",
"visibility": "PRIVATE",
"ownerId": "USERID",
"repositoryId": "TPLID",
"includeAllBranches": false,
}, inputs)
}),
)
r.Register(
httpmock.GraphQL(`mutation UpdateRepository\b`),
httpmock.GraphQLMutation(
`{
"data": {
"updateRepository": {
"repository": {
"id": "REPOID"
}
}
}
}`,
func(inputs map[string]interface{}) {
assert.Equal(t, map[string]interface{}{
"repositoryId": "REPOID",
"hasIssuesEnabled": false,
"hasWikiEnabled": true,
}, inputs)
}),
)
},
wantRepo: "https://github.com/OWNER/REPO",
},
{
name: "create personal repo from template repo, and disable both wiki and issues",
hostname: "github.com",
input: repoCreateInput{
Name: "gen-project",
Description: "my generated project",
Visibility: "private",
TemplateRepositoryID: "TPLID",
HasIssuesEnabled: false,
HasWikiEnabled: false,
IncludeAllBranches: false,
},
stubs: func(t *testing.T, r *httpmock.Registry) {
r.Register(
httpmock.GraphQL(`query UserCurrent\b`),
httpmock.StringResponse(`{"data": {"viewer": {"id":"USERID"} } }`))
r.Register(
httpmock.GraphQL(`mutation CloneTemplateRepository\b`),
httpmock.GraphQLMutation(
`{
"data": {
"cloneTemplateRepository": {
"repository": {
"id": "REPOID",
"name": "REPO",
"owner": {"login":"OWNER"},
"url": "the://URL"
}
}
}
}`,
func(inputs map[string]interface{}) {
assert.Equal(t, map[string]interface{}{
"name": "gen-project",
"description": "my generated project",
"visibility": "PRIVATE",
"ownerId": "USERID",
"repositoryId": "TPLID",
"includeAllBranches": false,
}, inputs)
}),
)
r.Register(
httpmock.GraphQL(`mutation UpdateRepository\b`),
httpmock.GraphQLMutation(
`{
"data": {
"updateRepository": {
"repository": {
"id": "REPOID"
}
}
}
}`,
func(inputs map[string]interface{}) {
assert.Equal(t, map[string]interface{}{
"repositoryId": "REPOID",
"hasIssuesEnabled": false,
"hasWikiEnabled": false,
}, inputs)
}),
)
},
wantRepo: "https://github.com/OWNER/REPO",
},
{
name: "create personal repo from template repo, and set homepage url",
hostname: "github.com",
input: repoCreateInput{
Name: "gen-project",
Description: "my generated project",
Visibility: "private",
TemplateRepositoryID: "TPLID",
HasIssuesEnabled: true,
HasWikiEnabled: true,
IncludeAllBranches: false,
HomepageURL: "https://example.com",
},
stubs: func(t *testing.T, r *httpmock.Registry) {
r.Register(
httpmock.GraphQL(`query UserCurrent\b`),
httpmock.StringResponse(`{"data": {"viewer": {"id":"USERID"} } }`))
r.Register(
httpmock.GraphQL(`mutation CloneTemplateRepository\b`),
httpmock.GraphQLMutation(
`{
"data": {
"cloneTemplateRepository": {
"repository": {
"id": "REPOID",
"name": "REPO",
"owner": {"login":"OWNER"},
"url": "the://URL"
}
}
}
}`,
func(inputs map[string]interface{}) {
assert.Equal(t, map[string]interface{}{
"name": "gen-project",
"description": "my generated project",
"visibility": "PRIVATE",
"ownerId": "USERID",
"repositoryId": "TPLID",
"includeAllBranches": false,
}, inputs)
}),
)
r.Register(
httpmock.GraphQL(`mutation UpdateRepository\b`),
httpmock.GraphQLMutation(
`{
"data": {
"updateRepository": {
"repository": {
"id": "REPOID"
}
}
}
}`,
func(inputs map[string]interface{}) {
assert.Equal(t, map[string]interface{}{
"repositoryId": "REPOID",
"hasIssuesEnabled": true,
"hasWikiEnabled": true,
"homepageUrl": "https://example.com",
}, inputs)
}),
)
},
wantRepo: "https://github.com/OWNER/REPO",
},
{
name: "create org repo from template repo",
hostname: "github.com",
input: repoCreateInput{
Name: "gen-project",
Description: "my generated project",
Visibility: "internal",
OwnerLogin: "myorg",
TemplateRepositoryID: "TPLID",
HasIssuesEnabled: true,
HasWikiEnabled: true,
IncludeAllBranches: false,
},
stubs: func(t *testing.T, r *httpmock.Registry) {
r.Register(
httpmock.REST("GET", "users/myorg"),
httpmock.StringResponse(`{ "node_id": "ORGID", "type": "Organization" }`))
r.Register(
httpmock.GraphQL(`mutation CloneTemplateRepository\b`),
httpmock.GraphQLMutation(
`{
"data": {
"cloneTemplateRepository": {
"repository": {
"id": "REPOID",
"name": "REPO",
"owner": {"login":"OWNER"},
"url": "the://URL"
}
}
}
}`,
func(inputs map[string]interface{}) {
assert.Equal(t, map[string]interface{}{
"name": "gen-project",
"description": "my generated project",
"visibility": "INTERNAL",
"ownerId": "ORGID",
"repositoryId": "TPLID",
"includeAllBranches": false,
}, inputs)
}),
)
},
wantRepo: "https://github.com/OWNER/REPO",
},
{
name: "create with license and gitignore",
hostname: "github.com",
input: repoCreateInput{
Name: "crisps",
Visibility: "private",
LicenseTemplate: "lgpl-3.0",
GitIgnoreTemplate: "Go",
HasIssuesEnabled: true,
HasWikiEnabled: true,
},
stubs: func(t *testing.T, r *httpmock.Registry) {
r.Register(
httpmock.REST("POST", "user/repos"),
httpmock.RESTPayload(201, `{"name":"crisps", "owner":{"login": "snacks-inc"}, "html_url":"the://URL"}`, func(payload map[string]interface{}) {
assert.Equal(t, map[string]interface{}{
"name": "crisps",
"private": true,
"gitignore_template": "Go",
"license_template": "lgpl-3.0",
"has_issues": true,
"has_wiki": true,
}, payload)
}))
},
wantRepo: "https://github.com/snacks-inc/crisps",
},
{
name: "create with README",
hostname: "github.com",
input: repoCreateInput{
Name: "crisps",
InitReadme: true,
},
stubs: func(t *testing.T, r *httpmock.Registry) {
r.Register(
httpmock.REST("POST", "user/repos"),
httpmock.RESTPayload(201, `{"name":"crisps", "owner":{"login": "snacks-inc"}, "html_url":"the://URL"}`, func(payload map[string]interface{}) {
assert.Equal(t, map[string]interface{}{
"name": "crisps",
"private": false,
"has_issues": false,
"has_wiki": false,
"auto_init": true,
}, payload)
}))
},
wantRepo: "https://github.com/snacks-inc/crisps",
},
{
name: "create with license and gitignore on Enterprise",
hostname: "example.com",
input: repoCreateInput{
Name: "crisps",
Visibility: "private",
LicenseTemplate: "lgpl-3.0",
GitIgnoreTemplate: "Go",
HasIssuesEnabled: true,
HasWikiEnabled: true,
},
stubs: func(t *testing.T, r *httpmock.Registry) {
r.Register(
httpmock.REST("POST", "api/v3/user/repos"),
httpmock.RESTPayload(201, `{"name":"crisps", "owner":{"login": "snacks-inc"}, "html_url":"the://URL"}`, func(payload map[string]interface{}) {
assert.Equal(t, map[string]interface{}{
"name": "crisps",
"private": true,
"gitignore_template": "Go",
"license_template": "lgpl-3.0",
"has_issues": true,
"has_wiki": true,
}, payload)
}))
},
wantRepo: "https://example.com/snacks-inc/crisps",
},
{
name: "create with license and gitignore in org",
hostname: "github.com",
input: repoCreateInput{
Name: "crisps",
Visibility: "INTERNAL",
OwnerLogin: "snacks-inc",
LicenseTemplate: "lgpl-3.0",
GitIgnoreTemplate: "Go",
HasIssuesEnabled: true,
HasWikiEnabled: true,
},
stubs: func(t *testing.T, r *httpmock.Registry) {
r.Register(
httpmock.REST("GET", "users/snacks-inc"),
httpmock.StringResponse(`{ "node_id": "ORGID", "type": "Organization" }`))
r.Register(
httpmock.REST("POST", "orgs/snacks-inc/repos"),
httpmock.RESTPayload(201, `{"name":"crisps", "owner":{"login": "snacks-inc"}, "html_url":"the://URL"}`, func(payload map[string]interface{}) {
assert.Equal(t, map[string]interface{}{
"name": "crisps",
"private": false,
"visibility": "internal",
"gitignore_template": "Go",
"license_template": "lgpl-3.0",
"has_issues": true,
"has_wiki": true,
}, payload)
}))
},
wantRepo: "https://github.com/snacks-inc/crisps",
},
{
name: "create with license and gitignore for team",
hostname: "github.com",
input: repoCreateInput{
Name: "crisps",
Visibility: "internal",
OwnerLogin: "snacks-inc",
TeamSlug: "munchies",
LicenseTemplate: "lgpl-3.0",
GitIgnoreTemplate: "Go",
HasIssuesEnabled: true,
HasWikiEnabled: true,
},
stubs: func(t *testing.T, r *httpmock.Registry) {
r.Register(
httpmock.REST("GET", "orgs/snacks-inc/teams/munchies"),
httpmock.StringResponse(`{ "node_id": "TEAMID", "id": 1234, "organization": {"node_id": "ORGID"} }`))
r.Register(
httpmock.REST("POST", "orgs/snacks-inc/repos"),
httpmock.RESTPayload(201, `{"name":"crisps", "owner":{"login": "snacks-inc"}, "html_url":"the://URL"}`, func(payload map[string]interface{}) {
assert.Equal(t, map[string]interface{}{
"name": "crisps",
"private": false,
"visibility": "internal",
"gitignore_template": "Go",
"license_template": "lgpl-3.0",
"team_id": float64(1234),
"has_issues": true,
"has_wiki": true,
}, payload)
}))
},
wantRepo: "https://github.com/snacks-inc/crisps",
},
{
name: "create personal repository but try to set it as 'internal'",
hostname: "github.com",
input: repoCreateInput{
Name: "winter-foods",
Description: "roasted chestnuts",
HomepageURL: "http://example.com",
Visibility: "internal",
OwnerLogin: "OWNER",
},
wantErr: true,
errMsg: "internal repositories can only be created within an organization",
stubs: func(t *testing.T, r *httpmock.Registry) {
r.Register(
httpmock.REST("GET", "users/OWNER"),
httpmock.StringResponse(`{ "node_id": "1234", "type": "Not-Org" }`))
r.Exclude(
t,
httpmock.GraphQL(`mutation RepositoryCreate\b`),
)
},
},
{
name: "create personal repository with README but try to set it as 'internal'",
hostname: "github.com",
input: repoCreateInput{
Name: "winter-foods",
Description: "roasted chestnuts",
HomepageURL: "http://example.com",
Visibility: "internal",
OwnerLogin: "OWNER",
InitReadme: true,
},
wantErr: true,
errMsg: "internal repositories can only be created within an organization",
stubs: func(t *testing.T, r *httpmock.Registry) {
r.Register(
httpmock.REST("GET", "users/OWNER"),
httpmock.StringResponse(`{ "node_id": "1234", "type": "Not-Org" }`))
r.Exclude(
t,
httpmock.REST("POST", "user/repos"),
)
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
reg := &httpmock.Registry{}
defer reg.Verify(t)
tt.stubs(t, reg)
httpClient := &http.Client{Transport: reg}
r, err := repoCreate(httpClient, tt.hostname, tt.input)
if tt.wantErr {
assert.Error(t, err)
if tt.errMsg != "" {
assert.ErrorContains(t, err, tt.errMsg)
}
return
} else {
assert.NoError(t, err)
}
assert.Equal(t, tt.wantRepo, ghrepo.GenerateRepoURL(r, ""))
})
}
}