1@cache_clear
2Feature: hub api
3  Background:
4    Given I am "octokitten" on github.com with OAuth token "OTOKEN"
5
6  Scenario: GET resource
7    Given the GitHub API server:
8      """
9      get('/hello/world') {
10        halt 401 unless request.env['HTTP_AUTHORIZATION'] == 'token OTOKEN'
11        halt 401 unless request.env['HTTP_ACCEPT'] == 'application/vnd.github.v3+json;charset=utf-8'
12        json :name => "Ed"
13      }
14      """
15    When I successfully run `hub api hello/world`
16    Then the output should contain exactly:
17      """
18      {"name":"Ed"}
19      """
20
21  Scenario: GET Enterprise resource
22    Given I am "octokitten" on git.my.org with OAuth token "FITOKEN"
23    Given the GitHub API server:
24      """
25      get('/api/v3/hello/world', :host_name => 'git.my.org') {
26        halt 401 unless request.env['HTTP_AUTHORIZATION'] == 'token FITOKEN'
27        json :name => "Ed"
28      }
29      """
30    And $GITHUB_HOST is "git.my.org"
31    When I successfully run `hub api hello/world`
32    Then the output should contain exactly:
33      """
34      {"name":"Ed"}
35      """
36
37  Scenario: Non-success response
38    Given the GitHub API server:
39      """
40      get('/hello/world') {
41        status 400
42        json :name => "Ed"
43      }
44      """
45    When I run `hub api hello/world`
46    Then the exit status should be 22
47    And the stdout should contain exactly:
48      """
49      {"name":"Ed"}
50      """
51    And the stderr should contain exactly ""
52
53  Scenario: Non-success response flat output
54    Given the GitHub API server:
55      """
56      get('/hello/world') {
57        status 400
58        json :name => "Ed"
59      }
60      """
61    When I run `hub api -t hello/world`
62    Then the exit status should be 22
63    And the stdout should contain exactly:
64      """
65      .name	Ed\n
66      """
67    And the stderr should contain exactly ""
68
69  Scenario: Non-success response doesn't choke on non-JSON
70    Given the GitHub API server:
71      """
72      get('/hello/world') {
73        status 400
74        content_type :text
75        'Something went wrong'
76      }
77      """
78    When I run `hub api -t hello/world`
79    Then the exit status should be 22
80    And the stdout should contain exactly:
81      """
82      Something went wrong
83      """
84    And the stderr should contain exactly ""
85
86  Scenario: GET query string
87    Given the GitHub API server:
88      """
89      get('/hello/world') {
90        json Hash[*params.sort.flatten]
91      }
92      """
93    When I successfully run `hub api -XGET -Fname=Ed -Fnum=12 -Fbool=false -Fvoid=null hello/world`
94    Then the output should contain exactly:
95      """
96      {"bool":"false","name":"Ed","num":"12","void":""}
97      """
98
99  Scenario: GET full URL
100    Given the GitHub API server:
101      """
102      get('/hello/world', :host_name => 'api.github.com') {
103        halt 401 unless request.env['HTTP_AUTHORIZATION'] == 'token OTOKEN'
104        json :name => "Faye"
105      }
106      """
107    When I successfully run `hub api https://api.github.com/hello/world`
108    Then the output should contain exactly:
109      """
110      {"name":"Faye"}
111      """
112
113  Scenario: Paginate REST
114    Given the GitHub API server:
115      """
116      get('/comments') {
117        assert :per_page => "6"
118        page = (params[:page] || 1).to_i
119        response.headers["Link"] = %(<#{request.url}&page=#{page+1}>; rel="next") if page < 3
120        json [{:page => page}]
121      }
122      """
123    When I successfully run `hub api --paginate comments?per_page=6`
124    Then the output should contain exactly:
125      """
126      [{"page":1}]
127      [{"page":2}]
128      [{"page":3}]
129      """
130
131  Scenario: Paginate GraphQL
132    Given the GitHub API server:
133      """
134      post('/graphql') {
135        variables = params[:variables] || {}
136        page = (variables["endCursor"] || 1).to_i
137        json :data => {
138          :pageInfo => {
139            :hasNextPage => page < 3,
140            :endCursor => (page+1).to_s
141          }
142        }
143      }
144      """
145    When I successfully run `hub api --paginate graphql -f query=QUERY`
146    Then the output should contain exactly:
147      """
148      {"data":{"pageInfo":{"hasNextPage":true,"endCursor":"2"}}}
149      {"data":{"pageInfo":{"hasNextPage":true,"endCursor":"3"}}}
150      {"data":{"pageInfo":{"hasNextPage":false,"endCursor":"4"}}}
151      """
152
153  Scenario: Avoid leaking token to a 3rd party
154    Given the GitHub API server:
155      """
156      get('/hello/world', :host_name => 'example.com') {
157        halt 401 unless request.env['HTTP_AUTHORIZATION'].nil?
158        json :name => "Jet"
159      }
160      """
161    When I successfully run `hub api http://example.com/hello/world`
162    Then the output should contain exactly:
163      """
164      {"name":"Jet"}
165      """
166
167  Scenario: Request headers
168    Given the GitHub API server:
169      """
170      get('/hello/world') {
171        json :accept => request.env['HTTP_ACCEPT'],
172             :foo => request.env['HTTP_X_FOO']
173      }
174      """
175      When I successfully run `hub api hello/world -H 'x-foo:bar' -H 'Accept: text/json'`
176    Then the output should contain exactly:
177      """
178      {"accept":"text/json","foo":"bar"}
179      """
180
181  Scenario: Response headers
182    Given the GitHub API server:
183      """
184      get('/hello/world') {
185        json({})
186      }
187      """
188    When I successfully run `hub api hello/world -i`
189    Then the output should contain "HTTP/1.1 200 OK"
190    And the output should contain "Content-Length: 2"
191
192  Scenario: POST fields
193    Given the GitHub API server:
194      """
195      post('/hello/world') {
196        json Hash[*params.sort.flatten]
197      }
198      """
199    When I successfully run `hub api -f name=@hubot -Fnum=12 -Fbool=false -Fvoid=null hello/world`
200    Then the output should contain exactly:
201      """
202      {"bool":false,"name":"@hubot","num":12,"void":null}
203      """
204
205  Scenario: POST raw fields
206    Given the GitHub API server:
207      """
208      post('/hello/world') {
209        json Hash[*params.sort.flatten]
210      }
211      """
212    When I successfully run `hub api -fnum=12 -fbool=false hello/world`
213    Then the output should contain exactly:
214      """
215      {"bool":"false","num":"12"}
216      """
217
218  Scenario: POST from stdin
219    Given the GitHub API server:
220      """
221      post('/graphql') {
222        json :query => params[:query]
223      }
224      """
225    When I run `hub api -t -F query=@- graphql` interactively
226    And I pass in:
227      """
228      query {
229        repository
230      }
231      """
232    Then the output should contain exactly:
233      """
234      .query	query {\n  repository\n}\n
235      """
236
237  Scenario: POST body from file
238    Given the GitHub API server:
239      """
240      post('/create') {
241        params[:obj].inspect
242      }
243      """
244    Given a file named "payload.json" with:
245      """
246      {"obj": ["one", 2, null]}
247      """
248    When I successfully run `hub api create --input payload.json`
249    Then the output should contain exactly:
250      """
251      ["one", 2, nil]
252      """
253
254  Scenario: POST body from stdin
255    Given the GitHub API server:
256      """
257      post('/create') {
258        params[:obj].inspect
259      }
260      """
261    When I run `hub api create --input -` interactively
262    And I pass in:
263      """
264      {"obj": {"name": "Ein", "datadog": true}}
265      """
266    Then the output should contain exactly:
267      """
268      {"name"=>"Ein", "datadog"=>true}
269      """
270
271  Scenario: Pass extra GraphQL variables
272    Given the GitHub API server:
273      """
274      post('/graphql') {
275        json(params[:variables])
276      }
277      """
278    When I successfully run `hub api -F query='query {}' -Fname=Jet -Fsize=2 graphql`
279    Then the output should contain exactly:
280      """
281      {"name":"Jet","size":2}
282      """
283
284  Scenario: Enterprise GraphQL
285    Given I am "octokitten" on git.my.org with OAuth token "FITOKEN"
286    Given the GitHub API server:
287      """
288      post('/api/graphql', :host_name => 'git.my.org') {
289        halt 401 unless request.env['HTTP_AUTHORIZATION'] == 'token FITOKEN'
290        json :name => "Ed"
291      }
292      """
293    And $GITHUB_HOST is "git.my.org"
294    When I successfully run `hub api graphql -f query=QUERY`
295    Then the output should contain exactly:
296      """
297      {"name":"Ed"}
298      """
299
300  Scenario: Repo context
301    Given I am in "git://github.com/octocat/Hello-World.git" git repo
302    Given the GitHub API server:
303      """
304      get('/repos/octocat/Hello-World/commits') {
305        json :commits => 12
306      }
307      """
308    When I successfully run `hub api repos/{owner}/{repo}/commits`
309    Then the output should contain exactly:
310      """
311      {"commits":12}
312      """
313
314  Scenario: Multiple string interpolation
315    Given I am in "git://github.com/octocat/Hello-World.git" git repo
316    Given the GitHub API server:
317      """
318      get('/repos/octocat/Hello-World/pulls') {
319        json(params)
320      }
321      """
322    When I successfully run `hub api repos/{owner}/{repo}/pulls?head={owner}:{repo}`
323    Then the output should contain exactly:
324      """
325      {"head":"octocat:Hello-World"}
326      """
327
328  Scenario: Repo context in graphql
329    Given I am in "git://github.com/octocat/Hello-World.git" git repo
330    Given the GitHub API server:
331      """
332      post('/graphql') {
333        json :query => params[:query]
334      }
335      """
336    When I run `hub api -t -F query=@- graphql` interactively
337    And I pass in:
338      """
339      repository(owner: "{owner}", name: "{repo}", nameWithOwner: "{owner}/{repo}")
340      """
341    Then the output should contain exactly:
342      """
343      .query	repository(owner: "octocat", name: "Hello-World", nameWithOwner: "octocat/Hello-World")\n
344      """
345
346  Scenario: Cache response
347    Given the GitHub API server:
348      """
349      count = 0
350      get('/count') {
351        count += 1
352        json :count => count
353      }
354      """
355    When I run `hub api -t 'count?a=1&b=2' --cache 5`
356    Then it should pass with ".count	1"
357    When I run `hub api -t 'count?b=2&a=1' --cache 5`
358    Then it should pass with ".count	1"
359
360  Scenario: Cache graphql response
361    Given the GitHub API server:
362      """
363      count = 0
364      post('/graphql') {
365        halt 400 unless params[:query] =~ /^Q\d$/
366        count += 1
367        json :count => count
368      }
369      """
370    When I run `hub api -t graphql -F query=Q1 --cache 5`
371    Then it should pass with ".count	1"
372    When I run `hub api -t graphql -F query=Q1 --cache 5`
373    Then it should pass with ".count	1"
374    When I run `hub api -t graphql -F query=Q2 --cache 5`
375    Then it should pass with ".count	2"
376
377  Scenario: Cache client error response
378    Given the GitHub API server:
379      """
380      count = 0
381      get('/count') {
382        count += 1
383        status 404 if count == 1
384        json :count => count
385      }
386      """
387    When I run `hub api -t count --cache 5`
388    Then it should fail with ".count	1"
389    When I run `hub api -t count --cache 5`
390    Then it should fail with ".count	1"
391    And the exit status should be 22
392
393  Scenario: Avoid caching server error response
394    Given the GitHub API server:
395      """
396      count = 0
397      get('/count') {
398        count += 1
399        status 500 if count == 1
400        json :count => count
401      }
402      """
403    When I run `hub api -t count --cache 5`
404    Then it should fail with ".count	1"
405    When I run `hub api -t count --cache 5`
406    Then it should pass with ".count	2"
407    When I run `hub api -t count --cache 5`
408    Then it should pass with ".count	2"
409
410  Scenario: Avoid caching response if the OAuth token changes
411    Given the GitHub API server:
412      """
413      count = 0
414      get('/count') {
415        count += 1
416        json :count => count
417      }
418      """
419    When I run `hub api -t count --cache 5`
420    Then it should pass with ".count	1"
421    Given I am "octocat" on github.com with OAuth token "TOKEN2"
422    When I run `hub api -t count --cache 5`
423    Then it should pass with ".count	2"
424
425  Scenario: Honor rate limit with pagination
426    Given the GitHub API server:
427      """
428      get('/hello') {
429        page = (params[:page] || 1).to_i
430        if page < 2
431          response.headers['X-Ratelimit-Remaining'] = '0'
432          response.headers['X-Ratelimit-Reset'] = Time.now.utc.to_i.to_s
433          response.headers['Link'] = %(</hello?page=#{page+1}>; rel="next")
434        end
435        json [{}]
436      }
437      """
438    When I successfully run `hub api --obey-ratelimit --paginate hello`
439    Then the stderr should contain "API rate limit exceeded; pausing until "
440
441  Scenario: Succumb to rate limit with pagination
442    Given the GitHub API server:
443      """
444      get('/hello') {
445        page = (params[:page] || 1).to_i
446        response.headers['X-Ratelimit-Remaining'] = '0'
447        response.headers['X-Ratelimit-Reset'] = Time.now.utc.to_i.to_s
448        if page == 2
449          status 403
450          json :message => "API rate limit exceeded"
451        else
452          response.headers['Link'] = %(</hello?page=#{page+1}>; rel="next")
453          json [{page:page}]
454        end
455      }
456      """
457    When I run `hub api --paginate -t hello`
458    Then the exit status should be 22
459    And the stderr should not contain "API rate limit exceeded"
460    And the stdout should contain exactly:
461      """
462      .[0].page	1
463      .message	API rate limit exceeded\n
464      """
465
466  Scenario: Honor rate limit for 403s
467    Given the GitHub API server:
468      """
469      count = 0
470      get('/hello') {
471        count += 1
472        if count == 1
473          response.headers['X-Ratelimit-Remaining'] = '0'
474          response.headers['X-Ratelimit-Reset'] = Time.now.utc.to_i.to_s
475          halt 403
476        end
477        json [{}]
478      }
479      """
480    When I successfully run `hub api --obey-ratelimit hello`
481    Then the stderr should contain "API rate limit exceeded; pausing until "
482
483  Scenario: 403 unrelated to rate limit
484    Given the GitHub API server:
485      """
486      get('/hello') {
487        response.headers['X-Ratelimit-Remaining'] = '1'
488        status 403
489      }
490      """
491    When I run `hub api --obey-ratelimit hello`
492    Then the exit status should be 22
493    Then the stderr should not contain "API rate limit exceeded"
494
495  Scenario: Warn about insufficient OAuth scopes
496    Given the GitHub API server:
497      """
498      get('/hello') {
499        response.headers['X-Accepted-Oauth-Scopes'] = 'repo, admin'
500        response.headers['X-Oauth-Scopes'] = 'public_repo'
501        status 403
502        json({})
503      }
504      """
505    When I run `hub api hello`
506    Then the exit status should be 22
507    And the output should contain exactly:
508      """
509      {}
510      Your access token may have insufficient scopes. Visit http://github.com/settings/tokens
511      to edit the 'hub' token and enable one of the following scopes: admin, repo
512      """
513
514  Scenario: Print the SSO challenge to stderr
515    Given the GitHub API server:
516      """
517      get('/orgs/acme') {
518        response.headers['X-GitHub-SSO'] = 'required; url=http://example.com?auth=HASH'
519        status 403
520        json({})
521      }
522      """
523    When I run `hub api orgs/acme`
524    Then the exit status should be 22
525    And the stderr should contain exactly:
526      """
527
528      You must authorize your token to access this organization:
529      http://example.com?auth=HASH
530      """
531