fbe 0.23.5 → 0.23.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.github/workflows/typos.yml +1 -1
- data/lib/fbe/middleware/rate_limit.rb +131 -0
- data/lib/fbe/middleware/trace.rb +7 -1
- data/lib/fbe/octo.rb +5 -3
- data/lib/fbe.rb +1 -1
- data/test/fbe/middleware/test_rate_limit.rb +152 -0
- data/test/fbe/middleware/test_trace.rb +62 -0
- data/test/fbe/test_conclude.rb +1 -5
- data/test/fbe/test_octo.rb +136 -28
- metadata +3 -1
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 36afd4e19f2a84e050e41ba6395a35cbc1bd404c8f6c1971affd64aa4c9c9e27
|
4
|
+
data.tar.gz: 904a216825c9bb411036db7c2486e492a6a58ca6b77f26b05f39c37cae377867
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: a3de96beb257378efe098e4ba1406a06dab673c6717ef249fd92bd31b92483f983847af33e6b34d00b707c4bddf2271f8afee4a597802e9c9d80300cd343625c
|
7
|
+
data.tar.gz: ce3036d1e93ca6585bc188724c9e34552ce5bc658c8d15ffe5e74d0cf59682322475941cac47e33c76338da5e07d84a44d4f0b0b407084c7918e26f7421b1a54
|
data/.github/workflows/typos.yml
CHANGED
@@ -0,0 +1,131 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# SPDX-FileCopyrightText: Copyright (c) 2024-2025 Zerocracy
|
4
|
+
# SPDX-License-Identifier: MIT
|
5
|
+
|
6
|
+
require 'faraday'
|
7
|
+
require 'json'
|
8
|
+
require_relative '../../fbe'
|
9
|
+
require_relative '../../fbe/middleware'
|
10
|
+
|
11
|
+
# Faraday middleware that caches GitHub API rate limit information.
|
12
|
+
#
|
13
|
+
# This middleware intercepts calls to the /rate_limit endpoint and caches
|
14
|
+
# the results locally. It tracks the remaining requests count and decrements
|
15
|
+
# it for each API call. Every 100 requests, it refreshes the cached data
|
16
|
+
# by allowing the request to pass through to the GitHub API.
|
17
|
+
#
|
18
|
+
# @example Usage in Faraday middleware stack
|
19
|
+
# connection = Faraday.new do |f|
|
20
|
+
# f.use Fbe::Middleware::RateLimit
|
21
|
+
# end
|
22
|
+
#
|
23
|
+
# Author:: Yegor Bugayenko (yegor256@gmail.com)
|
24
|
+
# Copyright:: Copyright (c) 2024-2025 Zerocracy
|
25
|
+
# License:: MIT
|
26
|
+
class Fbe::Middleware::RateLimit < Faraday::Middleware
|
27
|
+
# Initializes the rate limit middleware.
|
28
|
+
#
|
29
|
+
# @param [Object] app The next middleware in the stack
|
30
|
+
def initialize(app)
|
31
|
+
super
|
32
|
+
@cached_response = nil
|
33
|
+
@remaining_count = nil
|
34
|
+
@request_counter = 0
|
35
|
+
end
|
36
|
+
|
37
|
+
# Processes the HTTP request and handles rate limit caching.
|
38
|
+
#
|
39
|
+
# @param [Faraday::Env] env The request environment
|
40
|
+
# @return [Faraday::Response] The response from cache or the next middleware
|
41
|
+
def call(env)
|
42
|
+
if env.url.path == '/rate_limit'
|
43
|
+
handle_rate_limit_request(env)
|
44
|
+
else
|
45
|
+
track_request
|
46
|
+
@app.call(env)
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
private
|
51
|
+
|
52
|
+
# Handles requests to the rate_limit endpoint.
|
53
|
+
#
|
54
|
+
# @param [Faraday::Env] env The request environment
|
55
|
+
# @return [Faraday::Response] Cached or fresh response
|
56
|
+
def handle_rate_limit_request(env)
|
57
|
+
if @cached_response.nil? || @request_counter >= 100
|
58
|
+
response = @app.call(env)
|
59
|
+
@cached_response = response.dup
|
60
|
+
@remaining_count = extract_remaining_count(response)
|
61
|
+
@request_counter = 0
|
62
|
+
response
|
63
|
+
else
|
64
|
+
response = @cached_response.dup
|
65
|
+
update_remaining_count(response)
|
66
|
+
Faraday::Response.new(response_env(env, response))
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
# Tracks non-rate_limit requests and decrements counter.
|
71
|
+
def track_request
|
72
|
+
return if @remaining_count.nil?
|
73
|
+
@remaining_count -= 1 if @remaining_count.positive?
|
74
|
+
@request_counter += 1
|
75
|
+
end
|
76
|
+
|
77
|
+
# Extracts the remaining count from the response body.
|
78
|
+
#
|
79
|
+
# @param [Faraday::Response] response The API response
|
80
|
+
# @return [Integer] The remaining requests count
|
81
|
+
def extract_remaining_count(response)
|
82
|
+
body = response.body
|
83
|
+
if body.is_a?(String)
|
84
|
+
begin
|
85
|
+
body = JSON.parse(body)
|
86
|
+
rescue JSON::ParserError
|
87
|
+
return 0
|
88
|
+
end
|
89
|
+
end
|
90
|
+
return 0 unless body.is_a?(Hash)
|
91
|
+
body.dig('rate', 'remaining') || 0
|
92
|
+
end
|
93
|
+
|
94
|
+
# Updates the remaining count in the response body.
|
95
|
+
#
|
96
|
+
# @param [Faraday::Response] response The cached response to update
|
97
|
+
def update_remaining_count(response)
|
98
|
+
body = response.body
|
99
|
+
original_was_string = body.is_a?(String)
|
100
|
+
if original_was_string
|
101
|
+
begin
|
102
|
+
body = JSON.parse(body)
|
103
|
+
rescue JSON::ParserError
|
104
|
+
return
|
105
|
+
end
|
106
|
+
end
|
107
|
+
return unless body.is_a?(Hash) && body['rate']
|
108
|
+
body['rate']['remaining'] = @remaining_count
|
109
|
+
return unless original_was_string
|
110
|
+
response.instance_variable_set(:@body, body.to_json)
|
111
|
+
end
|
112
|
+
|
113
|
+
# Creates a response environment for the cached response.
|
114
|
+
#
|
115
|
+
# @param [Faraday::Env] env The original request environment
|
116
|
+
# @param [Faraday::Response] response The cached response
|
117
|
+
# @return [Hash] Response environment hash
|
118
|
+
def response_env(env, response)
|
119
|
+
headers = response.headers.dup
|
120
|
+
headers['x-ratelimit-remaining'] = @remaining_count.to_s if @remaining_count
|
121
|
+
{
|
122
|
+
method: env.method,
|
123
|
+
url: env.url,
|
124
|
+
request_headers: env.request_headers,
|
125
|
+
request_body: env.request_body,
|
126
|
+
status: response.status,
|
127
|
+
response_headers: headers,
|
128
|
+
body: response.body
|
129
|
+
}
|
130
|
+
end
|
131
|
+
end
|
data/lib/fbe/middleware/trace.rb
CHANGED
@@ -31,9 +31,12 @@ class Fbe::Middleware::Trace < Faraday::Middleware
|
|
31
31
|
#
|
32
32
|
# @param [Object] app The next middleware in the stack
|
33
33
|
# @param [Array] trace The array to store trace entries
|
34
|
-
|
34
|
+
# @param [Array<Symbol>] ignores The array of symbols (see Faraday::HttpCache::CACHE_STATUSES),
|
35
|
+
# which will be ignored
|
36
|
+
def initialize(app, trace, ignores: [])
|
35
37
|
super(app)
|
36
38
|
@trace = trace
|
39
|
+
@ignores = ignores
|
37
40
|
end
|
38
41
|
|
39
42
|
# Processes the HTTP request and records trace information.
|
@@ -47,6 +50,9 @@ class Fbe::Middleware::Trace < Faraday::Middleware
|
|
47
50
|
started_at: Time.now
|
48
51
|
}
|
49
52
|
@app.call(env).on_complete do |response_env|
|
53
|
+
next if !@ignores.empty? &&
|
54
|
+
response_env[:http_cache_trace] &&
|
55
|
+
(response_env[:http_cache_trace] & @ignores).size.positive?
|
50
56
|
finished = Time.now
|
51
57
|
duration = finished - entry[:started_at]
|
52
58
|
entry[:status] = response_env.status
|
data/lib/fbe/octo.rb
CHANGED
@@ -20,6 +20,7 @@ require 'verbose'
|
|
20
20
|
require_relative '../fbe'
|
21
21
|
require_relative 'middleware'
|
22
22
|
require_relative 'middleware/formatter'
|
23
|
+
require_relative 'middleware/rate_limit'
|
23
24
|
require_relative 'middleware/sqlite_store'
|
24
25
|
require_relative 'middleware/trace'
|
25
26
|
|
@@ -85,6 +86,10 @@ def Fbe.octo(options: $options, global: $global, loog: $loog)
|
|
85
86
|
methods: [:get],
|
86
87
|
backoff_factor: 2
|
87
88
|
)
|
89
|
+
builder.use(Octokit::Response::RaiseError)
|
90
|
+
builder.use(Faraday::Response::Logger, loog, formatter: Fbe::Middleware::Formatter)
|
91
|
+
builder.use(Fbe::Middleware::RateLimit)
|
92
|
+
builder.use(Fbe::Middleware::Trace, trace, ignores: [:fresh])
|
88
93
|
if options.sqlite_cache
|
89
94
|
maxsize = Filesize.from(options.sqlite_cache_maxsize || '10M').to_i
|
90
95
|
maxvsize = Filesize.from(options.sqlite_cache_maxvsize || '10K').to_i
|
@@ -105,9 +110,6 @@ def Fbe.octo(options: $options, global: $global, loog: $loog)
|
|
105
110
|
serializer: Marshal, shared_cache: false, logger: Loog::NULL
|
106
111
|
)
|
107
112
|
end
|
108
|
-
builder.use(Octokit::Response::RaiseError)
|
109
|
-
builder.use(Faraday::Response::Logger, loog, formatter: Fbe::Middleware::Formatter)
|
110
|
-
builder.use(Fbe::Middleware::Trace, trace)
|
111
113
|
builder.adapter(Faraday.default_adapter)
|
112
114
|
end
|
113
115
|
o.middleware = stack
|
data/lib/fbe.rb
CHANGED
@@ -0,0 +1,152 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# SPDX-FileCopyrightText: Copyright (c) 2024-2025 Zerocracy
|
4
|
+
# SPDX-License-Identifier: MIT
|
5
|
+
|
6
|
+
require 'faraday'
|
7
|
+
require 'webmock'
|
8
|
+
require_relative '../../test__helper'
|
9
|
+
require_relative '../../../lib/fbe'
|
10
|
+
require_relative '../../../lib/fbe/middleware'
|
11
|
+
require_relative '../../../lib/fbe/middleware/rate_limit'
|
12
|
+
|
13
|
+
# Test.
|
14
|
+
# Author:: Yegor Bugayenko (yegor256@gmail.com)
|
15
|
+
# Copyright:: Copyright (c) 2024-2025 Zerocracy
|
16
|
+
# License:: MIT
|
17
|
+
class RateLimitTest < Fbe::Test
|
18
|
+
def test_caches_rate_limit_response_on_first_call
|
19
|
+
rate_limit_response = {
|
20
|
+
'rate' => {
|
21
|
+
'limit' => 5000,
|
22
|
+
'remaining' => 4999,
|
23
|
+
'reset' => 1_672_531_200
|
24
|
+
}
|
25
|
+
}
|
26
|
+
stub_request(:get, 'https://api.github.com/rate_limit')
|
27
|
+
.to_return(status: 200, body: rate_limit_response.to_json, headers: { 'Content-Type' => 'application/json' })
|
28
|
+
conn = create_connection
|
29
|
+
response = conn.get('/rate_limit')
|
30
|
+
assert_equal 200, response.status
|
31
|
+
assert_equal 4999, response.body['rate']['remaining']
|
32
|
+
end
|
33
|
+
|
34
|
+
def test_returns_cached_response_on_subsequent_calls
|
35
|
+
rate_limit_response = {
|
36
|
+
'rate' => {
|
37
|
+
'limit' => 5000,
|
38
|
+
'remaining' => 4999,
|
39
|
+
'reset' => 1_672_531_200
|
40
|
+
}
|
41
|
+
}
|
42
|
+
stub_request(:get, 'https://api.github.com/rate_limit')
|
43
|
+
.to_return(status: 200, body: rate_limit_response.to_json, headers: { 'Content-Type' => 'application/json' })
|
44
|
+
.times(1)
|
45
|
+
conn = create_connection
|
46
|
+
conn.get('/rate_limit')
|
47
|
+
response = conn.get('/rate_limit')
|
48
|
+
assert_equal 200, response.status
|
49
|
+
assert_equal 4999, response.body['rate']['remaining']
|
50
|
+
assert_requested :get, 'https://api.github.com/rate_limit', times: 1
|
51
|
+
end
|
52
|
+
|
53
|
+
def test_decrements_remaining_count_for_non_rate_limit_requests
|
54
|
+
rate_limit_response = {
|
55
|
+
'rate' => {
|
56
|
+
'limit' => 5000,
|
57
|
+
'remaining' => 4999,
|
58
|
+
'reset' => 1_672_531_200
|
59
|
+
}
|
60
|
+
}
|
61
|
+
stub_request(:get, 'https://api.github.com/rate_limit')
|
62
|
+
.to_return(status: 200, body: rate_limit_response.to_json, headers: { 'Content-Type' => 'application/json',
|
63
|
+
'X-RateLimit-Remaining' => '4999' })
|
64
|
+
stub_request(:get, 'https://api.github.com/user')
|
65
|
+
.to_return(status: 200, body: '{"login": "test"}', headers: { 'Content-Type' => 'application/json' })
|
66
|
+
conn = create_connection
|
67
|
+
conn.get('/rate_limit')
|
68
|
+
conn.get('/user')
|
69
|
+
response = conn.get('/rate_limit')
|
70
|
+
assert_equal 4998, response.body['rate']['remaining']
|
71
|
+
assert_equal '4998', response.headers['x-ratelimit-remaining']
|
72
|
+
end
|
73
|
+
|
74
|
+
def test_refreshes_cache_after_hundred_requests
|
75
|
+
rate_limit_response = {
|
76
|
+
'rate' => {
|
77
|
+
'limit' => 5000,
|
78
|
+
'remaining' => 4999,
|
79
|
+
'reset' => 1_672_531_200
|
80
|
+
}
|
81
|
+
}
|
82
|
+
refreshed_response = {
|
83
|
+
'rate' => {
|
84
|
+
'limit' => 5000,
|
85
|
+
'remaining' => 4950,
|
86
|
+
'reset' => 1_672_531_200
|
87
|
+
}
|
88
|
+
}
|
89
|
+
stub_request(:get, 'https://api.github.com/rate_limit')
|
90
|
+
.to_return(status: 200, body: rate_limit_response.to_json, headers: { 'Content-Type' => 'application/json' })
|
91
|
+
.then
|
92
|
+
.to_return(status: 200, body: refreshed_response.to_json, headers: { 'Content-Type' => 'application/json' })
|
93
|
+
stub_request(:get, 'https://api.github.com/user')
|
94
|
+
.to_return(status: 200, body: '{"login": "test"}', headers: { 'Content-Type' => 'application/json' })
|
95
|
+
.times(100)
|
96
|
+
conn = create_connection
|
97
|
+
conn.get('/rate_limit')
|
98
|
+
100.times { conn.get('/user') }
|
99
|
+
response = conn.get('/rate_limit')
|
100
|
+
assert_equal 4950, response.body['rate']['remaining']
|
101
|
+
assert_requested :get, 'https://api.github.com/rate_limit', times: 2
|
102
|
+
end
|
103
|
+
|
104
|
+
def test_handles_response_without_rate_data
|
105
|
+
stub_request(:get, 'https://api.github.com/rate_limit')
|
106
|
+
.to_return(status: 200, body: '{}', headers: { 'Content-Type' => 'application/json' })
|
107
|
+
conn = create_connection
|
108
|
+
response = conn.get('/rate_limit')
|
109
|
+
assert_equal 200, response.status
|
110
|
+
assert_empty(response.body)
|
111
|
+
end
|
112
|
+
|
113
|
+
def test_ignores_non_hash_response_body
|
114
|
+
stub_request(:get, 'https://api.github.com/rate_limit')
|
115
|
+
.to_return(status: 200, body: 'invalid json', headers: { 'Content-Type' => 'text/plain' })
|
116
|
+
conn = create_connection
|
117
|
+
response = conn.get('/rate_limit')
|
118
|
+
assert_equal 200, response.status
|
119
|
+
assert_equal 'invalid json', response.body
|
120
|
+
end
|
121
|
+
|
122
|
+
def test_handles_zero_remaining_count
|
123
|
+
rate_limit_response = {
|
124
|
+
'rate' => {
|
125
|
+
'limit' => 5000,
|
126
|
+
'remaining' => 1,
|
127
|
+
'reset' => 1_672_531_200
|
128
|
+
}
|
129
|
+
}
|
130
|
+
stub_request(:get, 'https://api.github.com/rate_limit')
|
131
|
+
.to_return(status: 200, body: rate_limit_response.to_json, headers: { 'Content-Type' => 'application/json' })
|
132
|
+
stub_request(:get, 'https://api.github.com/user')
|
133
|
+
.to_return(status: 200, body: '{"login": "test"}', headers: { 'Content-Type' => 'application/json' })
|
134
|
+
.times(2)
|
135
|
+
conn = create_connection
|
136
|
+
conn.get('/rate_limit')
|
137
|
+
conn.get('/user')
|
138
|
+
conn.get('/user')
|
139
|
+
response = conn.get('/rate_limit')
|
140
|
+
assert_equal 0, response.body['rate']['remaining']
|
141
|
+
end
|
142
|
+
|
143
|
+
private
|
144
|
+
|
145
|
+
def create_connection
|
146
|
+
Faraday.new(url: 'https://api.github.com') do |f|
|
147
|
+
f.use Fbe::Middleware::RateLimit
|
148
|
+
f.response :json
|
149
|
+
f.adapter :net_http
|
150
|
+
end
|
151
|
+
end
|
152
|
+
end
|
@@ -4,6 +4,7 @@
|
|
4
4
|
# SPDX-License-Identifier: MIT
|
5
5
|
|
6
6
|
require 'faraday'
|
7
|
+
require 'faraday/http_cache'
|
7
8
|
require 'webmock'
|
8
9
|
require_relative '../../test__helper'
|
9
10
|
require_relative '../../../lib/fbe'
|
@@ -103,4 +104,65 @@ class TraceTest < Fbe::Test
|
|
103
104
|
assert_includes url, 'q=test'
|
104
105
|
assert_includes url, 'page=2'
|
105
106
|
end
|
107
|
+
|
108
|
+
def test_trace_and_cache_middlewares_together
|
109
|
+
WebMock.disable_net_connect!
|
110
|
+
now = Time.now
|
111
|
+
stub_request(:get, 'https://api.example.com/page')
|
112
|
+
.to_return(
|
113
|
+
status: 200,
|
114
|
+
headers: {
|
115
|
+
'date' => now.httpdate,
|
116
|
+
'cache-control' => 'public, max-age=60, s-maxage=60',
|
117
|
+
'last-modified' => (now - (6 * 60 * 60)).httpdate
|
118
|
+
},
|
119
|
+
body: 'some body 1'
|
120
|
+
)
|
121
|
+
.times(1)
|
122
|
+
.then
|
123
|
+
.to_return(
|
124
|
+
status: 200,
|
125
|
+
headers: {
|
126
|
+
'date' => (now + 70).httpdate,
|
127
|
+
'cache-control' => 'public, max-age=60, s-maxage=60',
|
128
|
+
'last-modified' => (now - (6 * 60 * 60)).httpdate,
|
129
|
+
'content-type' => 'application/json; charset=utf-8'
|
130
|
+
},
|
131
|
+
body: 'some body 2'
|
132
|
+
)
|
133
|
+
.times(1)
|
134
|
+
.then.to_raise('no more request to /page')
|
135
|
+
trace_real = []
|
136
|
+
trace_full = []
|
137
|
+
builder =
|
138
|
+
Faraday::RackBuilder.new do |f|
|
139
|
+
f.use Fbe::Middleware::Trace, trace_full
|
140
|
+
f.use(Faraday::HttpCache, serializer: Marshal, shared_cache: false, logger: Loog::NULL)
|
141
|
+
f.use Fbe::Middleware::Trace, trace_real
|
142
|
+
f.adapter :net_http
|
143
|
+
end
|
144
|
+
conn = Faraday::Connection.new(builder: builder)
|
145
|
+
5.times do
|
146
|
+
r = conn.get('https://api.example.com/page')
|
147
|
+
assert_equal('some body 1', r.body)
|
148
|
+
end
|
149
|
+
assert_equal(1, trace_real.size)
|
150
|
+
assert_equal(5, trace_full.size)
|
151
|
+
trace_real.clear
|
152
|
+
trace_full.clear
|
153
|
+
5.times do
|
154
|
+
r = conn.get('https://api.example.com/page')
|
155
|
+
assert_equal('some body 1', r.body)
|
156
|
+
end
|
157
|
+
assert_equal(0, trace_real.size)
|
158
|
+
assert_equal(5, trace_full.size)
|
159
|
+
Time.stub(:now, now + 70) do
|
160
|
+
5.times do
|
161
|
+
r = conn.get('https://api.example.com/page')
|
162
|
+
assert_equal('some body 2', r.body)
|
163
|
+
end
|
164
|
+
end
|
165
|
+
assert_equal(1, trace_real.size)
|
166
|
+
assert_equal(10, trace_full.size)
|
167
|
+
end
|
106
168
|
end
|
data/test/fbe/test_conclude.rb
CHANGED
@@ -79,11 +79,7 @@ class TestConclude < Fbe::Test
|
|
79
79
|
}
|
80
80
|
)
|
81
81
|
stub_request(:get, 'https://api.github.com/rate_limit').to_return(
|
82
|
-
{ body: '
|
83
|
-
{ body: 'hm...', headers: { 'X-RateLimit-Remaining' => '777' } },
|
84
|
-
{ body: 'hm...', headers: { 'X-RateLimit-Remaining' => '777' } },
|
85
|
-
{ body: 'hm...', headers: { 'X-RateLimit-Remaining' => '999' } },
|
86
|
-
{ body: 'hm...', headers: { 'X-RateLimit-Remaining' => '9' } }
|
82
|
+
{ body: '{"rate":{"remaining":51}}', headers: { 'X-RateLimit-Remaining' => '51' } }
|
87
83
|
)
|
88
84
|
global = {}
|
89
85
|
o = Fbe.octo(loog: Loog::NULL, options:, global:)
|
data/test/fbe/test_octo.rb
CHANGED
@@ -99,7 +99,7 @@ class TestOcto < Fbe::Test
|
|
99
99
|
def test_caching
|
100
100
|
WebMock.disable_net_connect!
|
101
101
|
stub_request(:get, 'https://api.github.com/rate_limit').to_return(
|
102
|
-
{ body: '{}', headers: { 'X-RateLimit-Remaining' => '222' } }
|
102
|
+
{ body: '{"rate":{"remaining":222}}', headers: { 'X-RateLimit-Remaining' => '222' } }
|
103
103
|
)
|
104
104
|
global = {}
|
105
105
|
o = Fbe.octo(loog: Loog::NULL, global:, options: Judges::Options.new)
|
@@ -133,12 +133,10 @@ class TestOcto < Fbe::Test
|
|
133
133
|
def test_off_quota?
|
134
134
|
WebMock.disable_net_connect!
|
135
135
|
stub_request(:get, 'https://api.github.com/rate_limit').to_return(
|
136
|
-
{ body: '{}', headers: { 'X-RateLimit-Remaining' => '
|
137
|
-
{ body: '{}', headers: { 'X-RateLimit-Remaining' => '333' } },
|
138
|
-
{ body: '{}', headers: { 'X-RateLimit-Remaining' => '3' } }
|
136
|
+
{ body: '{"rate":{"remaining":50}}', headers: { 'X-RateLimit-Remaining' => '50' } }
|
139
137
|
)
|
140
138
|
stub_request(:get, 'https://api.github.com/user/42').to_return(
|
141
|
-
body: '', headers: { 'X-RateLimit-Remaining' => '
|
139
|
+
body: '', headers: { 'X-RateLimit-Remaining' => '49' }
|
142
140
|
)
|
143
141
|
o = Fbe.octo(loog: Loog::NULL, global: {}, options: Judges::Options.new)
|
144
142
|
refute_predicate(o, :off_quota?)
|
@@ -149,11 +147,7 @@ class TestOcto < Fbe::Test
|
|
149
147
|
def test_off_quota_twice
|
150
148
|
WebMock.disable_net_connect!
|
151
149
|
stub_request(:get, 'https://api.github.com/rate_limit').to_return(
|
152
|
-
{ body: '{}', headers: { 'X-RateLimit-Remaining' => '
|
153
|
-
{ body: '{}', headers: { 'X-RateLimit-Remaining' => '333' } },
|
154
|
-
{ body: '{}', headers: { 'X-RateLimit-Remaining' => '333' } },
|
155
|
-
{ body: '{}', headers: { 'X-RateLimit-Remaining' => '5555' } },
|
156
|
-
{ body: '{}', headers: { 'X-RateLimit-Remaining' => '5' } }
|
150
|
+
{ body: '{"rate":{"remaining":51}}', headers: { 'X-RateLimit-Remaining' => '51' } }
|
157
151
|
)
|
158
152
|
stub_request(:get, 'https://api.github.com/user/42').to_return(
|
159
153
|
{ body: '', headers: { 'X-RateLimit-Remaining' => '5555' } },
|
@@ -372,9 +366,9 @@ class TestOcto < Fbe::Test
|
|
372
366
|
loog = Loog::Buffer.new
|
373
367
|
WebMock.disable_net_connect!
|
374
368
|
stub_request(:get, 'https://api.github.com/rate_limit').to_return(
|
375
|
-
{ body: '{}', headers: { 'X-RateLimit-Remaining' => '222' } },
|
376
|
-
{ body: '{}', headers: { 'X-RateLimit-Remaining' => '222' } },
|
377
|
-
{ body: '{}', headers: { 'X-RateLimit-Remaining' => '222' } }
|
369
|
+
{ body: '{"rate":{"remaining":222}}', headers: { 'X-RateLimit-Remaining' => '222' } },
|
370
|
+
{ body: '{"rate":{"remaining":222}}', headers: { 'X-RateLimit-Remaining' => '222' } },
|
371
|
+
{ body: '{"rate":{"remaining":222}}', headers: { 'X-RateLimit-Remaining' => '222' } }
|
378
372
|
)
|
379
373
|
stub_request(:get, 'https://api.github.com/user/123').to_return do
|
380
374
|
{
|
@@ -396,9 +390,9 @@ class TestOcto < Fbe::Test
|
|
396
390
|
octo.repository('foo/bar')
|
397
391
|
octo.print_trace!(all: true, max: 9_999)
|
398
392
|
output = loog.to_s
|
399
|
-
assert_includes output, '3 URLs vs
|
400
|
-
assert_includes output, '
|
401
|
-
assert_includes output, '/rate_limit:
|
393
|
+
assert_includes output, '3 URLs vs 4 requests'
|
394
|
+
assert_includes output, '219 quota left'
|
395
|
+
assert_includes output, '/rate_limit: 1'
|
402
396
|
assert_includes output, '/user/123: 1'
|
403
397
|
assert_includes output, '/repos/foo/bar: 2'
|
404
398
|
repo_index = output.index('/repos/foo/bar: 2')
|
@@ -427,12 +421,67 @@ class TestOcto < Fbe::Test
|
|
427
421
|
octo = Fbe.octo(loog: Loog::NULL, global: {}, options: Judges::Options.new({ 'sqlite_cache' => fcache }))
|
428
422
|
octo.user(123)
|
429
423
|
loog = Loog::Buffer.new
|
430
|
-
octo = Fbe.octo(loog
|
424
|
+
octo = Fbe.octo(loog:, global: {}, options: Judges::Options.new({ 'sqlite_cache' => fcache }))
|
431
425
|
WebMock.remove_request_stub(stub)
|
432
426
|
octo.user(123)
|
433
|
-
octo.print_trace!
|
434
|
-
|
427
|
+
octo.print_trace!(all: true)
|
428
|
+
refute_match('/user/123: 1', loog.to_s)
|
429
|
+
end
|
430
|
+
end
|
431
|
+
|
432
|
+
def test_octo_not_trace_cached_requests
|
433
|
+
WebMock.disable_net_connect!
|
434
|
+
now = Time.now
|
435
|
+
stub_request(:get, 'https://api.github.com/rate_limit')
|
436
|
+
.to_return(
|
437
|
+
status: 200, headers: { 'Content-Type' => 'application/json', 'X-RateLimit-Remaining' => '5000' },
|
438
|
+
body: { 'rate' => { 'limit' => 5000, 'remaining' => 5000, 'reset' => 1_672_531_200 } }.to_json
|
439
|
+
)
|
440
|
+
stub_request(:get, 'https://api.github.com/repos/zerocracy/baza.rb')
|
441
|
+
.to_return(
|
442
|
+
status: 200,
|
443
|
+
headers: {
|
444
|
+
'date' => now.httpdate,
|
445
|
+
'cache-control' => 'public, max-age=60, s-maxage=60',
|
446
|
+
'last-modified' => (now - (6 * 60 * 60)).httpdate,
|
447
|
+
'content-type' => 'application/json; charset=utf-8'
|
448
|
+
},
|
449
|
+
body: { id: 840_215_648, name: 'baza.rb' }.to_json
|
450
|
+
)
|
451
|
+
.times(1)
|
452
|
+
.then
|
453
|
+
.to_return(
|
454
|
+
status: 200,
|
455
|
+
headers: {
|
456
|
+
'date' => (now + 70).httpdate,
|
457
|
+
'cache-control' => 'public, max-age=60, s-maxage=60',
|
458
|
+
'last-modified' => (now - (6 * 60 * 60)).httpdate,
|
459
|
+
'content-type' => 'application/json; charset=utf-8'
|
460
|
+
},
|
461
|
+
body: { id: 840_215_648, name: 'baza.rb' }.to_json
|
462
|
+
)
|
463
|
+
.times(1)
|
464
|
+
.then.to_raise('no more request to /repos/zerocracy/baza.rb')
|
465
|
+
loog = Loog::Buffer.new
|
466
|
+
o = Fbe.octo(loog:, global: {}, options: Judges::Options.new({}))
|
467
|
+
o.print_trace!(all: true)
|
468
|
+
Time.stub(:now, now) do
|
469
|
+
5.times do
|
470
|
+
o.repo('zerocracy/baza.rb')
|
471
|
+
end
|
435
472
|
end
|
473
|
+
o.print_trace!(all: true)
|
474
|
+
Time.stub(:now, now + 70) do
|
475
|
+
25.times do
|
476
|
+
o.repo('zerocracy/baza.rb')
|
477
|
+
end
|
478
|
+
end
|
479
|
+
o.print_trace!(all: true)
|
480
|
+
assert_requested :get, 'https://api.github.com/repos/zerocracy/baza.rb', times: 2
|
481
|
+
output = loog.to_s
|
482
|
+
assert_match('/repos/zerocracy/baza.rb: 1', output)
|
483
|
+
refute_match('/repos/zerocracy/baza.rb: 5', output)
|
484
|
+
refute_match('/repos/zerocracy/baza.rb: 25', output)
|
436
485
|
end
|
437
486
|
|
438
487
|
def test_trace_gets_cleared_after_print
|
@@ -539,19 +588,78 @@ class TestOcto < Fbe::Test
|
|
539
588
|
def test_fetch_rate_limit_by_making_new_request
|
540
589
|
WebMock.disable_net_connect!
|
541
590
|
stub_request(:get, 'https://api.github.com/rate_limit').to_return(
|
542
|
-
{ body: '{}', headers: { 'X-RateLimit-Remaining' => '321' } }
|
543
|
-
)
|
544
|
-
{ body: '{}', headers: { 'X-RateLimit-Remaining' => '123' } }
|
545
|
-
).to_return(
|
546
|
-
{ body: '{}', headers: { 'X-RateLimit-Remaining' => '1' } }
|
547
|
-
).to_raise(StandardError.new('no more requests to https://api.github.com/rate_limit'))
|
591
|
+
{ body: '{"rate":{"remaining":321}}', headers: { 'X-RateLimit-Remaining' => '321' } }
|
592
|
+
)
|
548
593
|
loog = Loog::Buffer.new
|
549
594
|
o = Fbe.octo(loog:, global: {}, options: Judges::Options.new)
|
550
595
|
refute_predicate(o, :off_quota?)
|
551
596
|
assert_match(/321 GitHub API quota left/, loog.to_s)
|
552
597
|
o.print_trace!(all: true)
|
553
|
-
assert_match(/
|
554
|
-
|
555
|
-
|
598
|
+
assert_match(/321 quota left/, loog.to_s)
|
599
|
+
end
|
600
|
+
|
601
|
+
def test_throttling_request_to_rate_limit
|
602
|
+
WebMock.disable_net_connect!
|
603
|
+
stub_request(:get, 'https://api.github.com/rate_limit')
|
604
|
+
.to_return(
|
605
|
+
status: 200, headers: { 'Content-Type' => 'application/json', 'X-RateLimit-Remaining' => '5000' },
|
606
|
+
body: { 'rate' => { 'limit' => 5000, 'remaining' => 5000, 'reset' => 1_672_531_200 } }.to_json
|
607
|
+
)
|
608
|
+
.then.to_return(
|
609
|
+
status: 200, headers: { 'Content-Type' => 'application/json', 'X-RateLimit-Remaining' => '4900' },
|
610
|
+
body: { 'rate' => { 'limit' => 5000, 'remaining' => 4900, 'reset' => 1_672_531_200 } }.to_json
|
611
|
+
)
|
612
|
+
.then.to_return(
|
613
|
+
status: 200, headers: { 'Content-Type' => 'application/json', 'X-RateLimit-Remaining' => '4800' },
|
614
|
+
body: { 'rate' => { 'limit' => 5000, 'remaining' => 4800, 'reset' => 1_672_531_200 } }.to_json
|
615
|
+
)
|
616
|
+
.then.to_raise('no more request to /rate_limit')
|
617
|
+
stub_request(:get, 'https://api.github.com/user/1')
|
618
|
+
.to_return(
|
619
|
+
status: 200, headers: { 'Content-Type' => 'application/json' },
|
620
|
+
body: { 'id' => 1, 'login' => 'user1' }.to_json
|
621
|
+
).times(1)
|
622
|
+
stub_request(:get, 'https://api.github.com/user/111')
|
623
|
+
.to_return(
|
624
|
+
status: 200, headers: { 'Content-Type' => 'application/json' },
|
625
|
+
body: { 'id' => 111, 'login' => 'user111' }.to_json
|
626
|
+
)
|
627
|
+
.times(201)
|
628
|
+
.then.to_raise('no more request to /user/111')
|
629
|
+
loog = Loog::Buffer.new
|
630
|
+
o = Fbe.octo(loog:, global: {}, options: Judges::Options.new({}))
|
631
|
+
o.user(1)
|
632
|
+
o.print_trace!(all: true)
|
633
|
+
201.times do
|
634
|
+
o.user(111)
|
635
|
+
o.rate_limit!.remaining
|
636
|
+
end
|
637
|
+
o.print_trace!(all: true)
|
638
|
+
output = loog.to_s
|
639
|
+
assert_requested :get, 'https://api.github.com/user/1', times: 1
|
640
|
+
assert_requested :get, 'https://api.github.com/user/111', times: 201
|
641
|
+
assert_requested :get, 'https://api.github.com/rate_limit', times: 3
|
642
|
+
assert_match('2 URLs vs 2 requests', output)
|
643
|
+
assert_match('/user/1: 1', output)
|
644
|
+
assert_match('/rate_limit: 1', output)
|
645
|
+
assert_match('2 URLs vs 203 requests', output)
|
646
|
+
assert_match('/user/111: 201', output)
|
647
|
+
assert_match('/rate_limit: 2', output)
|
648
|
+
end
|
649
|
+
|
650
|
+
def test_octo_http_cache_middleware_located_in_end_of_chain
|
651
|
+
WebMock.disable_net_connect!
|
652
|
+
stub_request(:get, 'https://api.github.com/rate_limit')
|
653
|
+
.to_return(
|
654
|
+
status: 200, headers: { 'Content-Type' => 'application/json', 'X-RateLimit-Remaining' => '5000' },
|
655
|
+
body: { 'rate' => { 'limit' => 5000, 'remaining' => 5000, 'reset' => 1_672_531_200 } }.to_json
|
656
|
+
)
|
657
|
+
o = Fbe.octo(loog: fake_loog, global: {}, options: Judges::Options.new({}))
|
658
|
+
assert_equal('Faraday::HttpCache', o.middleware.handlers.last.name, <<~MSG.strip.gsub!(/\s+/, ' '))
|
659
|
+
Faraday::HttpCache middleware must be located in the end of chain middlewares,
|
660
|
+
because the Oktokit client change Faraday::HttpCache position to the last,
|
661
|
+
for more info, see: https://github.com/zerocracy/fbe/issues/230#issuecomment-3020551743 and
|
662
|
+
https://github.com/octokit/octokit.rb/blob/ea3413c3174571e87c83d358fc893cc7613091fa/lib/octokit/connection.rb#L109-L119
|
663
|
+
MSG
|
556
664
|
end
|
557
665
|
end
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: fbe
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.23.
|
4
|
+
version: 0.23.7
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Yegor Bugayenko
|
@@ -363,6 +363,7 @@ files:
|
|
363
363
|
- lib/fbe/just_one.rb
|
364
364
|
- lib/fbe/middleware.rb
|
365
365
|
- lib/fbe/middleware/formatter.rb
|
366
|
+
- lib/fbe/middleware/rate_limit.rb
|
366
367
|
- lib/fbe/middleware/sqlite_store.rb
|
367
368
|
- lib/fbe/middleware/trace.rb
|
368
369
|
- lib/fbe/octo.rb
|
@@ -376,6 +377,7 @@ files:
|
|
376
377
|
- renovate.json
|
377
378
|
- rules/basic.fe
|
378
379
|
- test/fbe/middleware/test_formatter.rb
|
380
|
+
- test/fbe/middleware/test_rate_limit.rb
|
379
381
|
- test/fbe/middleware/test_sqlite_store.rb
|
380
382
|
- test/fbe/middleware/test_trace.rb
|
381
383
|
- test/fbe/test_award.rb
|