Skip to content

Commit 2afc462

Browse files
authored
Merge pull request cli#2058 from wilso199/1942-api-hostname-flag
2 parents 4b395c5 + fbff1fc commit 2afc462

File tree

5 files changed

+105
-14
lines changed

5 files changed

+105
-14
lines changed

internal/ghinstance/host.go

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package ghinstance
22

33
import (
4+
"errors"
45
"fmt"
56
"strings"
67
)
@@ -42,6 +43,21 @@ func NormalizeHostname(h string) string {
4243
return hostname
4344
}
4445

46+
func HostnameValidator(v interface{}) error {
47+
hostname, valid := v.(string)
48+
if !valid {
49+
return errors.New("hostname is not a string")
50+
}
51+
52+
if len(strings.TrimSpace(hostname)) < 1 {
53+
return errors.New("a value is required")
54+
}
55+
if strings.ContainsRune(hostname, '/') || strings.ContainsRune(hostname, ':') {
56+
return errors.New("invalid hostname")
57+
}
58+
return nil
59+
}
60+
4561
func GraphQLEndpoint(hostname string) string {
4662
if IsEnterprise(hostname) {
4763
return fmt.Sprintf("https://%s/api/graphql", hostname)

internal/ghinstance/host_test.go

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@ package ghinstance
22

33
import (
44
"testing"
5+
6+
"github.com/stretchr/testify/assert"
57
)
68

79
func TestOverridableDefault(t *testing.T) {
@@ -97,6 +99,50 @@ func TestNormalizeHostname(t *testing.T) {
9799
}
98100
}
99101

102+
func TestHostnameValidator(t *testing.T) {
103+
tests := []struct {
104+
name string
105+
input interface{}
106+
wantsErr bool
107+
}{
108+
{
109+
name: "valid hostname",
110+
input: "internal.instance",
111+
wantsErr: false,
112+
},
113+
{
114+
name: "hostname with slashes",
115+
input: "//internal.instance",
116+
wantsErr: true,
117+
},
118+
{
119+
name: "empty hostname",
120+
input: " ",
121+
wantsErr: true,
122+
},
123+
{
124+
name: "hostname with colon",
125+
input: "internal.instance:2205",
126+
wantsErr: true,
127+
},
128+
{
129+
name: "non-string hostname",
130+
input: 62,
131+
wantsErr: true,
132+
},
133+
}
134+
135+
for _, tt := range tests {
136+
t.Run(tt.name, func(t *testing.T) {
137+
err := HostnameValidator(tt.input)
138+
if tt.wantsErr {
139+
assert.Error(t, err)
140+
return
141+
}
142+
assert.Equal(t, nil, err)
143+
})
144+
}
145+
}
100146
func TestGraphQLEndpoint(t *testing.T) {
101147
tests := []struct {
102148
host string

pkg/cmd/api/api.go

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ import (
2727
type ApiOptions struct {
2828
IO *iostreams.IOStreams
2929

30+
Hostname string
3031
RequestMethod string
3132
RequestMethodPassed bool
3233
RequestPath string
@@ -101,7 +102,7 @@ original query accepts an '$endCursor: String' variable and that it fetches the
101102
}
102103
}
103104
'
104-
105+
105106
$ gh api graphql --paginate -f query='
106107
query($endCursor: String) {
107108
viewer {
@@ -121,13 +122,21 @@ original query accepts an '$endCursor: String' variable and that it fetches the
121122
GITHUB_TOKEN: an authentication token for github.com API requests.
122123
123124
GITHUB_ENTERPRISE_TOKEN: an authentication token for API requests to GitHub Enterprise.
125+
126+
GH_HOST: make the request to a GitHub host other than github.com.
124127
`),
125128
},
126129
Args: cobra.ExactArgs(1),
127130
RunE: func(c *cobra.Command, args []string) error {
128131
opts.RequestPath = args[0]
129132
opts.RequestMethodPassed = c.Flags().Changed("method")
130133

134+
if c.Flags().Changed("hostname") {
135+
if err := ghinstance.HostnameValidator(opts.Hostname); err != nil {
136+
return &cmdutil.FlagError{Err: fmt.Errorf("error parsing --hostname: %w", err)}
137+
}
138+
}
139+
131140
if opts.Paginate && !strings.EqualFold(opts.RequestMethod, "GET") && opts.RequestPath != "graphql" {
132141
return &cmdutil.FlagError{Err: errors.New(`the '--paginate' option is not supported for non-GET requests`)}
133142
}
@@ -142,6 +151,7 @@ original query accepts an '$endCursor: String' variable and that it fetches the
142151
},
143152
}
144153

154+
cmd.Flags().StringVar(&opts.Hostname, "hostname", "", "The GitHub hostname for the request (default \"github.com\")")
145155
cmd.Flags().StringVarP(&opts.RequestMethod, "method", "X", "GET", "The HTTP method for the request")
146156
cmd.Flags().StringArrayVarP(&opts.MagicFields, "field", "F", nil, "Add a parameter of inferred type")
147157
cmd.Flags().StringArrayVarP(&opts.RawFields, "raw-field", "f", nil, "Add a string parameter")
@@ -206,6 +216,9 @@ func apiRun(opts *ApiOptions) error {
206216
}
207217

208218
host := ghinstance.OverridableDefault()
219+
if opts.Hostname != "" {
220+
host = opts.Hostname
221+
}
209222

210223
hasNextPage := true
211224
for hasNextPage {

pkg/cmd/api/api_test.go

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ func Test_NewCmdApi(t *testing.T) {
3131
name: "no flags",
3232
cli: "graphql",
3333
wants: ApiOptions{
34+
Hostname: "",
3435
RequestMethod: "GET",
3536
RequestMethodPassed: false,
3637
RequestPath: "graphql",
@@ -48,6 +49,7 @@ func Test_NewCmdApi(t *testing.T) {
4849
name: "override method",
4950
cli: "repos/octocat/Spoon-Knife -XDELETE",
5051
wants: ApiOptions{
52+
Hostname: "",
5153
RequestMethod: "DELETE",
5254
RequestMethodPassed: true,
5355
RequestPath: "repos/octocat/Spoon-Knife",
@@ -65,6 +67,7 @@ func Test_NewCmdApi(t *testing.T) {
6567
name: "with fields",
6668
cli: "graphql -f query=QUERY -F body=@file.txt",
6769
wants: ApiOptions{
70+
Hostname: "",
6871
RequestMethod: "GET",
6972
RequestMethodPassed: false,
7073
RequestPath: "graphql",
@@ -82,6 +85,7 @@ func Test_NewCmdApi(t *testing.T) {
8285
name: "with headers",
8386
cli: "user -H 'accept: text/plain' -i",
8487
wants: ApiOptions{
88+
Hostname: "",
8589
RequestMethod: "GET",
8690
RequestMethodPassed: false,
8791
RequestPath: "user",
@@ -99,6 +103,7 @@ func Test_NewCmdApi(t *testing.T) {
99103
name: "with pagination",
100104
cli: "repos/OWNER/REPO/issues --paginate",
101105
wants: ApiOptions{
106+
Hostname: "",
102107
RequestMethod: "GET",
103108
RequestMethodPassed: false,
104109
RequestPath: "repos/OWNER/REPO/issues",
@@ -116,6 +121,7 @@ func Test_NewCmdApi(t *testing.T) {
116121
name: "with silenced output",
117122
cli: "repos/OWNER/REPO/issues --silent",
118123
wants: ApiOptions{
124+
Hostname: "",
119125
RequestMethod: "GET",
120126
RequestMethodPassed: false,
121127
RequestPath: "repos/OWNER/REPO/issues",
@@ -138,6 +144,7 @@ func Test_NewCmdApi(t *testing.T) {
138144
name: "GraphQL pagination",
139145
cli: "-XPOST graphql --paginate",
140146
wants: ApiOptions{
147+
Hostname: "",
141148
RequestMethod: "POST",
142149
RequestMethodPassed: true,
143150
RequestPath: "graphql",
@@ -160,6 +167,7 @@ func Test_NewCmdApi(t *testing.T) {
160167
name: "with request body from file",
161168
cli: "user --input myfile",
162169
wants: ApiOptions{
170+
Hostname: "",
163171
RequestMethod: "GET",
164172
RequestMethodPassed: false,
165173
RequestPath: "user",
@@ -178,10 +186,29 @@ func Test_NewCmdApi(t *testing.T) {
178186
cli: "",
179187
wantsErr: true,
180188
},
189+
{
190+
name: "with hostname",
191+
cli: "graphql --hostname tom.petty",
192+
wants: ApiOptions{
193+
Hostname: "tom.petty",
194+
RequestMethod: "GET",
195+
RequestMethodPassed: false,
196+
RequestPath: "graphql",
197+
RequestInputFile: "",
198+
RawFields: []string(nil),
199+
MagicFields: []string(nil),
200+
RequestHeaders: []string(nil),
201+
ShowResponseHeaders: false,
202+
Paginate: false,
203+
Silent: false,
204+
},
205+
wantsErr: false,
206+
},
181207
}
182208
for _, tt := range tests {
183209
t.Run(tt.name, func(t *testing.T) {
184210
cmd := NewCmdApi(f, func(o *ApiOptions) error {
211+
assert.Equal(t, tt.wants.Hostname, o.Hostname)
185212
assert.Equal(t, tt.wants.RequestMethod, o.RequestMethod)
186213
assert.Equal(t, tt.wants.RequestMethodPassed, o.RequestMethodPassed)
187214
assert.Equal(t, tt.wants.RequestPath, o.RequestPath)

pkg/cmd/auth/login/login.go

Lines changed: 2 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -88,7 +88,7 @@ func NewCmdLogin(f *cmdutil.Factory, runF func(*LoginOptions) error) *cobra.Comm
8888
}
8989

9090
if cmd.Flags().Changed("hostname") {
91-
if err := hostnameValidator(opts.Hostname); err != nil {
91+
if err := ghinstance.HostnameValidator(opts.Hostname); err != nil {
9292
return &cmdutil.FlagError{Err: fmt.Errorf("error parsing --hostname: %w", err)}
9393
}
9494
}
@@ -166,7 +166,7 @@ func loginRun(opts *LoginOptions) error {
166166
if isEnterprise {
167167
err := prompt.SurveyAskOne(&survey.Input{
168168
Message: "GHE hostname:",
169-
}, &hostname, survey.WithValidator(hostnameValidator))
169+
}, &hostname, survey.WithValidator(ghinstance.HostnameValidator))
170170
if err != nil {
171171
return fmt.Errorf("could not prompt: %w", err)
172172
}
@@ -307,17 +307,6 @@ func loginRun(opts *LoginOptions) error {
307307
return nil
308308
}
309309

310-
func hostnameValidator(v interface{}) error {
311-
val := v.(string)
312-
if len(strings.TrimSpace(val)) < 1 {
313-
return errors.New("a value is required")
314-
}
315-
if strings.ContainsRune(val, '/') || strings.ContainsRune(val, ':') {
316-
return errors.New("invalid hostname")
317-
}
318-
return nil
319-
}
320-
321310
func getAccessTokenTip(hostname string) string {
322311
ghHostname := hostname
323312
if ghHostname == "" {

0 commit comments

Comments
 (0)