github.com/ungtb10d/cli/v2@v2.0.0-20221110210412-98537dd9d6a1/api/client_test.go (about)

     1  package api
     2  
     3  import (
     4  	"bytes"
     5  	"errors"
     6  	"io"
     7  	"net/http"
     8  	"net/http/httptest"
     9  	"testing"
    10  
    11  	"github.com/ungtb10d/cli/v2/pkg/httpmock"
    12  	"github.com/ungtb10d/cli/v2/pkg/iostreams"
    13  	"github.com/stretchr/testify/assert"
    14  )
    15  
    16  func newTestClient(reg *httpmock.Registry) *Client {
    17  	client := &http.Client{}
    18  	httpmock.ReplaceTripper(client, reg)
    19  	return NewClientFromHTTP(client)
    20  }
    21  
    22  func TestGraphQL(t *testing.T) {
    23  	http := &httpmock.Registry{}
    24  	client := newTestClient(http)
    25  
    26  	vars := map[string]interface{}{"name": "Mona"}
    27  	response := struct {
    28  		Viewer struct {
    29  			Login string
    30  		}
    31  	}{}
    32  
    33  	http.Register(
    34  		httpmock.GraphQL("QUERY"),
    35  		httpmock.StringResponse(`{"data":{"viewer":{"login":"hubot"}}}`),
    36  	)
    37  
    38  	err := client.GraphQL("github.com", "QUERY", vars, &response)
    39  	assert.NoError(t, err)
    40  	assert.Equal(t, "hubot", response.Viewer.Login)
    41  
    42  	req := http.Requests[0]
    43  	reqBody, _ := io.ReadAll(req.Body)
    44  	assert.Equal(t, `{"query":"QUERY","variables":{"name":"Mona"}}`, string(reqBody))
    45  }
    46  
    47  func TestGraphQLError(t *testing.T) {
    48  	reg := &httpmock.Registry{}
    49  	client := newTestClient(reg)
    50  
    51  	response := struct{}{}
    52  
    53  	reg.Register(
    54  		httpmock.GraphQL(""),
    55  		httpmock.StringResponse(`
    56  			{ "errors": [
    57  				{
    58  					"type": "NOT_FOUND",
    59  					"message": "OH NO",
    60  					"path": ["repository", "issue"]
    61  				},
    62  				{
    63  					"type": "ACTUALLY_ITS_FINE",
    64  					"message": "this is fine",
    65  					"path": ["repository", "issues", 0, "comments"]
    66  				}
    67  			  ]
    68  			}
    69  		`),
    70  	)
    71  
    72  	err := client.GraphQL("github.com", "", nil, &response)
    73  	if err == nil || err.Error() != "GraphQL: OH NO (repository.issue), this is fine (repository.issues.0.comments)" {
    74  		t.Fatalf("got %q", err.Error())
    75  	}
    76  }
    77  
    78  func TestRESTGetDelete(t *testing.T) {
    79  	http := &httpmock.Registry{}
    80  	client := newTestClient(http)
    81  
    82  	http.Register(
    83  		httpmock.REST("DELETE", "applications/CLIENTID/grant"),
    84  		httpmock.StatusStringResponse(204, "{}"),
    85  	)
    86  
    87  	r := bytes.NewReader([]byte(`{}`))
    88  	err := client.REST("github.com", "DELETE", "applications/CLIENTID/grant", r, nil)
    89  	assert.NoError(t, err)
    90  }
    91  
    92  func TestRESTWithFullURL(t *testing.T) {
    93  	http := &httpmock.Registry{}
    94  	client := newTestClient(http)
    95  
    96  	http.Register(
    97  		httpmock.REST("GET", "api/v3/user/repos"),
    98  		httpmock.StatusStringResponse(200, "{}"))
    99  	http.Register(
   100  		httpmock.REST("GET", "user/repos"),
   101  		httpmock.StatusStringResponse(200, "{}"))
   102  
   103  	err := client.REST("example.com", "GET", "user/repos", nil, nil)
   104  	assert.NoError(t, err)
   105  	err = client.REST("example.com", "GET", "https://another.net/user/repos", nil, nil)
   106  	assert.NoError(t, err)
   107  
   108  	assert.Equal(t, "example.com", http.Requests[0].URL.Hostname())
   109  	assert.Equal(t, "another.net", http.Requests[1].URL.Hostname())
   110  }
   111  
   112  func TestRESTError(t *testing.T) {
   113  	fakehttp := &httpmock.Registry{}
   114  	client := newTestClient(fakehttp)
   115  
   116  	fakehttp.Register(httpmock.MatchAny, func(req *http.Request) (*http.Response, error) {
   117  		return &http.Response{
   118  			Request:    req,
   119  			StatusCode: 422,
   120  			Body:       io.NopCloser(bytes.NewBufferString(`{"message": "OH NO"}`)),
   121  			Header: map[string][]string{
   122  				"Content-Type": {"application/json; charset=utf-8"},
   123  			},
   124  		}, nil
   125  	})
   126  
   127  	var httpErr HTTPError
   128  	err := client.REST("github.com", "DELETE", "repos/branch", nil, nil)
   129  	if err == nil || !errors.As(err, &httpErr) {
   130  		t.Fatalf("got %v", err)
   131  	}
   132  
   133  	if httpErr.StatusCode != 422 {
   134  		t.Errorf("expected status code 422, got %d", httpErr.StatusCode)
   135  	}
   136  	if httpErr.Error() != "HTTP 422: OH NO (https://api.github.com/repos/branch)" {
   137  		t.Errorf("got %q", httpErr.Error())
   138  	}
   139  }
   140  
   141  func TestHandleHTTPError_GraphQL502(t *testing.T) {
   142  	req, err := http.NewRequest("GET", "https://api.github.com/user", nil)
   143  	if err != nil {
   144  		t.Fatal(err)
   145  	}
   146  	resp := &http.Response{
   147  		Request:    req,
   148  		StatusCode: 502,
   149  		Body:       io.NopCloser(bytes.NewBufferString(`{ "data": null, "errors": [{ "message": "Something went wrong" }] }`)),
   150  		Header:     map[string][]string{"Content-Type": {"application/json"}},
   151  	}
   152  	err = HandleHTTPError(resp)
   153  	if err == nil || err.Error() != "HTTP 502: Something went wrong (https://api.github.com/user)" {
   154  		t.Errorf("got error: %v", err)
   155  	}
   156  }
   157  
   158  func TestHTTPError_ScopesSuggestion(t *testing.T) {
   159  	makeResponse := func(s int, u, haveScopes, needScopes string) *http.Response {
   160  		req, err := http.NewRequest("GET", u, nil)
   161  		if err != nil {
   162  			t.Fatal(err)
   163  		}
   164  		return &http.Response{
   165  			Request:    req,
   166  			StatusCode: s,
   167  			Body:       io.NopCloser(bytes.NewBufferString(`{}`)),
   168  			Header: map[string][]string{
   169  				"Content-Type":            {"application/json"},
   170  				"X-Oauth-Scopes":          {haveScopes},
   171  				"X-Accepted-Oauth-Scopes": {needScopes},
   172  			},
   173  		}
   174  	}
   175  
   176  	tests := []struct {
   177  		name string
   178  		resp *http.Response
   179  		want string
   180  	}{
   181  		{
   182  			name: "has necessary scopes",
   183  			resp: makeResponse(404, "https://api.github.com/gists", "repo, gist, read:org", "gist"),
   184  			want: ``,
   185  		},
   186  		{
   187  			name: "normalizes scopes",
   188  			resp: makeResponse(404, "https://api.github.com/orgs/ORG/discussions", "admin:org, write:discussion", "read:org, read:discussion"),
   189  			want: ``,
   190  		},
   191  		{
   192  			name: "no scopes on endpoint",
   193  			resp: makeResponse(404, "https://api.github.com/user", "repo", ""),
   194  			want: ``,
   195  		},
   196  		{
   197  			name: "missing a scope",
   198  			resp: makeResponse(404, "https://api.github.com/gists", "repo, read:org", "gist, delete_repo"),
   199  			want: `This API operation needs the "gist" scope. To request it, run:  gh auth refresh -h github.com -s gist`,
   200  		},
   201  		{
   202  			name: "server error",
   203  			resp: makeResponse(500, "https://api.github.com/gists", "repo", "gist"),
   204  			want: ``,
   205  		},
   206  		{
   207  			name: "no scopes on token",
   208  			resp: makeResponse(404, "https://api.github.com/gists", "", "gist, delete_repo"),
   209  			want: ``,
   210  		},
   211  		{
   212  			name: "http code is 422",
   213  			resp: makeResponse(422, "https://api.github.com/gists", "", "gist"),
   214  			want: "",
   215  		},
   216  	}
   217  	for _, tt := range tests {
   218  		t.Run(tt.name, func(t *testing.T) {
   219  			httpError := HandleHTTPError(tt.resp)
   220  			if got := httpError.(HTTPError).ScopesSuggestion(); got != tt.want {
   221  				t.Errorf("HTTPError.ScopesSuggestion() = %v, want %v", got, tt.want)
   222  			}
   223  		})
   224  	}
   225  }
   226  
   227  func TestHTTPHeaders(t *testing.T) {
   228  	var gotReq *http.Request
   229  	ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
   230  		gotReq = r
   231  		w.WriteHeader(http.StatusNoContent)
   232  	}))
   233  	defer ts.Close()
   234  
   235  	ios, _, _, stderr := iostreams.Test()
   236  	httpClient, err := NewHTTPClient(HTTPClientOptions{
   237  		AppVersion:        "v1.2.3",
   238  		Config:            tinyConfig{ts.URL[7:] + ":oauth_token": "MYTOKEN"},
   239  		Log:               ios.ErrOut,
   240  		SkipAcceptHeaders: false,
   241  	})
   242  	assert.NoError(t, err)
   243  	client := NewClientFromHTTP(httpClient)
   244  
   245  	err = client.REST(ts.URL, "GET", ts.URL+"/user/repos", nil, nil)
   246  	assert.NoError(t, err)
   247  
   248  	wantHeader := map[string]string{
   249  		"Accept":        "application/vnd.github.merge-info-preview+json, application/vnd.github.nebula-preview",
   250  		"Authorization": "token MYTOKEN",
   251  		"Content-Type":  "application/json; charset=utf-8",
   252  		"User-Agent":    "GitHub CLI v1.2.3",
   253  	}
   254  	for name, value := range wantHeader {
   255  		assert.Equal(t, value, gotReq.Header.Get(name), name)
   256  	}
   257  	assert.Equal(t, "", stderr.String())
   258  }