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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 157b3bb591e16d535883feb11b531f9f64a9b87dd078f858345a7f91cc29185d
4
- data.tar.gz: bb1b991edbcec6cf8caaedb11e32e8359d5f0487d1339572f0e43e4cf296354f
3
+ metadata.gz: 36afd4e19f2a84e050e41ba6395a35cbc1bd404c8f6c1971affd64aa4c9c9e27
4
+ data.tar.gz: 904a216825c9bb411036db7c2486e492a6a58ca6b77f26b05f39c37cae377867
5
5
  SHA512:
6
- metadata.gz: 9821935ea70538efc8847b7368ed2af195f40f348f07ab432f52712bdafa85b5c629455079d8697ca0c7b5a969c93233d99d133ae5744a1c7c7f359a26483d89
7
- data.tar.gz: 105e6d4f0934d8ce1b166e43417c258944f0e502ee6de1fa8487f9ccbe656f1f9f67c5a50a76403a98f89986576e44aad6bb9f12d75e8ee7a2d7e096b67ccdcb
6
+ metadata.gz: a3de96beb257378efe098e4ba1406a06dab673c6717ef249fd92bd31b92483f983847af33e6b34d00b707c4bddf2271f8afee4a597802e9c9d80300cd343625c
7
+ data.tar.gz: ce3036d1e93ca6585bc188724c9e34552ce5bc658c8d15ffe5e74d0cf59682322475941cac47e33c76338da5e07d84a44d4f0b0b407084c7918e26f7421b1a54
@@ -16,6 +16,6 @@ jobs:
16
16
  runs-on: ubuntu-24.04
17
17
  steps:
18
18
  - uses: actions/checkout@v4
19
- - uses: crate-ci/typos@v1.33.1
19
+ - uses: crate-ci/typos@v1.34.0
20
20
  with:
21
21
  config: .github/.typos.toml
@@ -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
@@ -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
- def initialize(app, trace)
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
@@ -10,5 +10,5 @@
10
10
  # License:: MIT
11
11
  module Fbe
12
12
  # Current version of the gem (changed by +.rultor.yml+ on every release)
13
- VERSION = '0.23.5' unless const_defined?(:VERSION)
13
+ VERSION = '0.23.7' unless const_defined?(:VERSION)
14
14
  end
@@ -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
@@ -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: 'hm...', headers: { 'X-RateLimit-Remaining' => '777' } },
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:)
@@ -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' => '333' } },
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' => '3' }
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' => '333' } },
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 6 requests'
400
- assert_includes output, '222 quota left'
401
- assert_includes output, '/rate_limit: 3'
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: fake_loog, global: {}, options: Judges::Options.new({ 'sqlite_cache' => fcache }))
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
- assert_empty(loog.to_s)
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
- ).to_return(
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(/123 quota left/, loog.to_s)
554
- assert_predicate(o, :off_quota?)
555
- assert_match(/1 GitHub API quota left/, loog.to_s)
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.5
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