cli/pkg/cmd/api/api_test.go
Mislav Marohnić d57b5171cf Print HTTP errors on stderr in api command
Most API errors are present in the response body itself, which will be
sent to stdout normally, but if stdout is redirected somewhere (as it's
common with scripts), failed HTTP requests will likely sabotage the rest
of the script, but no useful info will be shown on stderr.

This makes it so all REST and GraphQL errors are always shown on stderr.
Additionally, this makes sure that the command exits with a nonzero
status on any GraphQL errors.
2020-06-05 18:24:24 +02:00

332 lines
7.5 KiB
Go

package api
import (
"bytes"
"fmt"
"io"
"io/ioutil"
"net/http"
"os"
"testing"
"github.com/cli/cli/pkg/cmdutil"
"github.com/cli/cli/pkg/iostreams"
"github.com/google/shlex"
"github.com/stretchr/testify/assert"
)
func Test_NewCmdApi(t *testing.T) {
f := &cmdutil.Factory{}
tests := []struct {
name string
cli string
wants ApiOptions
wantsErr bool
}{
{
name: "no flags",
cli: "graphql",
wants: ApiOptions{
RequestMethod: "GET",
RequestMethodPassed: false,
RequestPath: "graphql",
RawFields: []string(nil),
MagicFields: []string(nil),
RequestHeaders: []string(nil),
ShowResponseHeaders: false,
},
wantsErr: false,
},
{
name: "override method",
cli: "repos/octocat/Spoon-Knife -XDELETE",
wants: ApiOptions{
RequestMethod: "DELETE",
RequestMethodPassed: true,
RequestPath: "repos/octocat/Spoon-Knife",
RawFields: []string(nil),
MagicFields: []string(nil),
RequestHeaders: []string(nil),
ShowResponseHeaders: false,
},
wantsErr: false,
},
{
name: "with fields",
cli: "graphql -f query=QUERY -F body=@file.txt",
wants: ApiOptions{
RequestMethod: "GET",
RequestMethodPassed: false,
RequestPath: "graphql",
RawFields: []string{"query=QUERY"},
MagicFields: []string{"body=@file.txt"},
RequestHeaders: []string(nil),
ShowResponseHeaders: false,
},
wantsErr: false,
},
{
name: "with headers",
cli: "user -H 'accept: text/plain' -i",
wants: ApiOptions{
RequestMethod: "GET",
RequestMethodPassed: false,
RequestPath: "user",
RawFields: []string(nil),
MagicFields: []string(nil),
RequestHeaders: []string{"accept: text/plain"},
ShowResponseHeaders: true,
},
wantsErr: false,
},
{
name: "no arguments",
cli: "",
wantsErr: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
cmd := NewCmdApi(f, func(o *ApiOptions) error {
assert.Equal(t, tt.wants.RequestMethod, o.RequestMethod)
assert.Equal(t, tt.wants.RequestMethodPassed, o.RequestMethodPassed)
assert.Equal(t, tt.wants.RequestPath, o.RequestPath)
assert.Equal(t, tt.wants.RawFields, o.RawFields)
assert.Equal(t, tt.wants.MagicFields, o.MagicFields)
assert.Equal(t, tt.wants.RequestHeaders, o.RequestHeaders)
assert.Equal(t, tt.wants.ShowResponseHeaders, o.ShowResponseHeaders)
return nil
})
argv, err := shlex.Split(tt.cli)
assert.NoError(t, err)
cmd.SetArgs(argv)
cmd.SetIn(&bytes.Buffer{})
cmd.SetOut(&bytes.Buffer{})
cmd.SetErr(&bytes.Buffer{})
_, err = cmd.ExecuteC()
if tt.wantsErr {
assert.Error(t, err)
return
}
assert.NoError(t, err)
})
}
}
func Test_apiRun(t *testing.T) {
tests := []struct {
name string
options ApiOptions
httpResponse *http.Response
err error
stdout string
stderr string
}{
{
name: "success",
httpResponse: &http.Response{
StatusCode: 200,
Body: ioutil.NopCloser(bytes.NewBufferString(`bam!`)),
},
err: nil,
stdout: `bam!`,
stderr: ``,
},
{
name: "show response headers",
options: ApiOptions{
ShowResponseHeaders: true,
},
httpResponse: &http.Response{
StatusCode: 200,
Body: ioutil.NopCloser(bytes.NewBufferString(`body`)),
Header: http.Header{"Content-Type": []string{"text/plain"}},
},
err: nil,
stdout: "Content-Type: text/plain\r\n\r\nbody",
stderr: ``,
},
{
name: "success 204",
httpResponse: &http.Response{
StatusCode: 204,
Body: nil,
},
err: nil,
stdout: ``,
stderr: ``,
},
{
name: "REST error",
httpResponse: &http.Response{
StatusCode: 400,
Body: ioutil.NopCloser(bytes.NewBufferString(`{"message": "THIS IS FINE"}`)),
Header: http.Header{"Content-Type": []string{"application/json; charset=utf-8"}},
},
err: cmdutil.SilentError,
stdout: `{"message": "THIS IS FINE"}`,
stderr: "gh: THIS IS FINE (HTTP 400)\n",
},
{
name: "GraphQL error",
options: ApiOptions{
RequestPath: "graphql",
},
httpResponse: &http.Response{
StatusCode: 200,
Body: ioutil.NopCloser(bytes.NewBufferString(`{"errors": [{"message":"AGAIN"}, {"message":"FINE"}]}`)),
Header: http.Header{"Content-Type": []string{"application/json; charset=utf-8"}},
},
err: cmdutil.SilentError,
stdout: `{"errors": [{"message":"AGAIN"}, {"message":"FINE"}]}`,
stderr: "gh: AGAIN\nFINE\n",
},
{
name: "failure",
httpResponse: &http.Response{
StatusCode: 502,
Body: ioutil.NopCloser(bytes.NewBufferString(`gateway timeout`)),
},
err: cmdutil.SilentError,
stdout: `gateway timeout`,
stderr: "gh: HTTP 502\n",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
io, _, stdout, stderr := iostreams.Test()
tt.options.IO = io
tt.options.HttpClient = func() (*http.Client, error) {
var tr roundTripper = func(req *http.Request) (*http.Response, error) {
resp := tt.httpResponse
resp.Request = req
return resp, nil
}
return &http.Client{Transport: tr}, nil
}
err := apiRun(&tt.options)
if err != tt.err {
t.Errorf("expected error %v, got %v", tt.err, err)
}
if stdout.String() != tt.stdout {
t.Errorf("expected output %q, got %q", tt.stdout, stdout.String())
}
if stderr.String() != tt.stderr {
t.Errorf("expected error output %q, got %q", tt.stderr, stderr.String())
}
})
}
}
func Test_parseFields(t *testing.T) {
io, stdin, _, _ := iostreams.Test()
fmt.Fprint(stdin, "pasted contents")
opts := ApiOptions{
IO: io,
RawFields: []string{
"robot=Hubot",
"destroyer=false",
"helper=true",
"location=@work",
},
MagicFields: []string{
"input=@-",
"enabled=true",
"victories=123",
},
}
params, err := parseFields(&opts)
if err != nil {
t.Fatalf("parseFields error: %v", err)
}
expect := map[string]interface{}{
"robot": "Hubot",
"destroyer": "false",
"helper": "true",
"location": "@work",
"input": []byte("pasted contents"),
"enabled": true,
"victories": 123,
}
assert.Equal(t, expect, params)
}
func Test_magicFieldValue(t *testing.T) {
f, err := ioutil.TempFile("", "gh-test")
if err != nil {
t.Fatal(err)
}
fmt.Fprint(f, "file contents")
f.Close()
t.Cleanup(func() { os.Remove(f.Name()) })
type args struct {
v string
stdin io.ReadCloser
}
tests := []struct {
name string
args args
want interface{}
wantErr bool
}{
{
name: "string",
args: args{v: "hello"},
want: "hello",
wantErr: false,
},
{
name: "bool true",
args: args{v: "true"},
want: true,
wantErr: false,
},
{
name: "bool false",
args: args{v: "false"},
want: false,
wantErr: false,
},
{
name: "null",
args: args{v: "null"},
want: nil,
wantErr: false,
},
{
name: "file",
args: args{v: "@" + f.Name()},
want: []byte("file contents"),
wantErr: false,
},
{
name: "file error",
args: args{v: "@"},
want: nil,
wantErr: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := magicFieldValue(tt.args.v, tt.args.stdin)
if (err != nil) != tt.wantErr {
t.Errorf("magicFieldValue() error = %v, wantErr %v", err, tt.wantErr)
return
}
if tt.wantErr {
return
}
assert.Equal(t, tt.want, got)
})
}
}