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 }