fbe 0.24.3 → 0.25.0

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: ce758baa2b569c87b5c6318bd2a58df0a5041c578164bb07f337c197a83ef4a7
4
- data.tar.gz: fa85c249bc4ea36ecf435e0412bccd9a1c8885ed222f0538be0c02ebd4f59ccc
3
+ metadata.gz: c7615e3d4257252fcc987acca6caeb717c328a79edfd563eb67e704be741cef2
4
+ data.tar.gz: 3a256df746d860b44a2d6f3d44cad34690dce87ca9743227b1e114edfcdff5bd
5
5
  SHA512:
6
- metadata.gz: 43e184c1ee6955ca80cd5a4ba01aa47cf5e59ce8ad1767c03e535c77c253a1bb9223d6603825ef99c6d094828d03e3ae7b6a68e3cc4b7766cfb108c3058fa714
7
- data.tar.gz: f1399b8d6b1022a2460f6c1049bd2b58aa924933773c0bdbec3cc21ba65e28aeca6712d382fdd070e0ec5f44f8c5ac3608aacdb9d87857c11f00b45bc6efd728
6
+ metadata.gz: 3bf3c8e87ea0f0bc52a51d7f89c60bc1dc5e67b78eccac3ce9de0ac80f73406b4b8fec508d7c83c10b42a07fbe594505d3a5cddc80fd8a35e00c292a80eaf911
7
+ data.tar.gz: 2b4b12a737a6f810d304727f649faa7dceaf224c317c7d79d01705af5442a9510ede60d9f63f9afc433b6b3ce3a4bd49a7c30934d2ee9b6c90d26aebbb1a9d84
data/.rubocop.yml CHANGED
@@ -56,3 +56,5 @@ Metrics/ParameterLists:
56
56
  Enabled: false
57
57
  Layout/MultilineAssignmentLayout:
58
58
  Enabled: true
59
+ Style/RequireOrder:
60
+ Enabled: true
data/Gemfile CHANGED
@@ -17,7 +17,7 @@ gem 'rubocop-minitest', '~>0.38', require: false
17
17
  gem 'rubocop-performance', '~>1.25', require: false
18
18
  gem 'rubocop-rake', '~>0.7', require: false
19
19
  gem 'simplecov', '~>0.22', require: false
20
- gem 'simplecov-cobertura', '~>2.1', require: false
20
+ gem 'simplecov-cobertura', '~>3.0', require: false
21
21
  gem 'veils', '~>0.4', require: false
22
22
  gem 'webmock', '~>3.25', require: false
23
23
  gem 'yard', '~>0.9', require: false
data/Gemfile.lock CHANGED
@@ -223,7 +223,7 @@ GEM
223
223
  docile (~> 1.1)
224
224
  simplecov-html (~> 0.11)
225
225
  simplecov_json_formatter (~> 0.1)
226
- simplecov-cobertura (2.1.0)
226
+ simplecov-cobertura (3.0.0)
227
227
  rexml
228
228
  simplecov (~> 0.19)
229
229
  simplecov-html (0.13.1)
@@ -277,7 +277,7 @@ DEPENDENCIES
277
277
  rubocop-performance (~> 1.25)
278
278
  rubocop-rake (~> 0.7)
279
279
  simplecov (~> 0.22)
280
- simplecov-cobertura (~> 2.1)
280
+ simplecov-cobertura (~> 3.0)
281
281
  veils (~> 0.4)
282
282
  webmock (~> 3.25)
283
283
  yard (~> 0.9)
data/Rakefile CHANGED
@@ -4,9 +4,9 @@
4
4
  # SPDX-License-Identifier: MIT
5
5
 
6
6
  require 'qbash'
7
- require 'rubygems'
8
7
  require 'rake'
9
8
  require 'rake/clean'
9
+ require 'rubygems'
10
10
 
11
11
  def name
12
12
  @name ||= File.basename(Dir['*.gemspec'].first, '.*')
data/lib/fbe/conclude.rb CHANGED
@@ -6,8 +6,8 @@
6
6
  require 'tago'
7
7
  require_relative '../fbe'
8
8
  require_relative 'fb'
9
- require_relative 'octo'
10
9
  require_relative 'if_absent'
10
+ require_relative 'octo'
11
11
 
12
12
  # Creates an instance of {Fbe::Conclude} and evals it with the block provided.
13
13
  #
data/lib/fbe/fb.rb CHANGED
@@ -4,11 +4,11 @@
4
4
  # SPDX-License-Identifier: MIT
5
5
 
6
6
  require 'factbase'
7
+ require 'factbase/cached/cached_factbase'
8
+ require 'factbase/indexed/indexed_factbase'
7
9
  require 'factbase/logged'
8
10
  require 'factbase/pre'
9
11
  require 'factbase/rules'
10
- require 'factbase/cached/cached_factbase'
11
- require 'factbase/indexed/indexed_factbase'
12
12
  require 'factbase/sync/sync_factbase'
13
13
  require 'judges'
14
14
  require 'loog'
data/lib/fbe/just_one.rb CHANGED
@@ -3,8 +3,8 @@
3
3
  # SPDX-FileCopyrightText: Copyright (c) 2024-2025 Zerocracy
4
4
  # SPDX-License-Identifier: MIT
5
5
 
6
- require 'time'
7
6
  require 'others'
7
+ require 'time'
8
8
  require_relative '../fbe'
9
9
  require_relative 'fb'
10
10
 
@@ -3,6 +3,7 @@
3
3
  # SPDX-FileCopyrightText: Copyright (c) 2024-2025 Zerocracy
4
4
  # SPDX-License-Identifier: MIT
5
5
 
6
+ require 'ellipsized'
6
7
  require 'faraday'
7
8
  require 'faraday/logging/formatter'
8
9
  require_relative '../../fbe'
@@ -66,6 +67,21 @@ class Fbe::Middleware::Formatter < Faraday::Logging::Formatter
66
67
  )
67
68
  return
68
69
  end
70
+ if http.status >= 500 && http.response_headers['content-type']&.start_with?('text')
71
+ error(
72
+ [
73
+ "#{@req.method.upcase} #{apply_filters(@req.url.to_s)} HTTP/1.1",
74
+ shifted(apply_filters(dump_headers(@req.request_headers))),
75
+ '',
76
+ shifted(apply_filters(@req.request_body)),
77
+ "HTTP/1.1 #{http.status}",
78
+ shifted(apply_filters(dump_headers(http.response_headers))),
79
+ '',
80
+ shifted(apply_filters(http.response_body&.ellipsized(100, :right)))
81
+ ].join("\n")
82
+ )
83
+ return
84
+ end
69
85
  error(
70
86
  [
71
87
  "#{@req.method.upcase} #{apply_filters(@req.url.to_s)} HTTP/1.1",
@@ -3,12 +3,12 @@
3
3
  # SPDX-FileCopyrightText: Copyright (c) 2024-2025 Zerocracy
4
4
  # SPDX-License-Identifier: MIT
5
5
 
6
- require 'zlib'
7
6
  require 'filesize'
8
7
  require 'json'
9
8
  require 'loog'
10
9
  require 'sqlite3'
11
10
  require 'time'
11
+ require 'zlib'
12
12
  require_relative '../../fbe'
13
13
  require_relative '../../fbe/middleware'
14
14
 
@@ -53,8 +53,11 @@ class Fbe::Middleware::SqliteStore
53
53
  # @param loog [Loog] Logger instance (optional, defaults to Loog::NULL)
54
54
  # @param maxsize [Integer] Maximum database size in bytes (optional, defaults to 10MB)
55
55
  # @param maxvsize [Integer] Maximum size in bytes of a single value (optional, defaults to 10Kb)
56
- # @raise [ArgumentError] If path is nil/empty, directory doesn't exist, or version is nil/empty
57
- def initialize(path, version, loog: Loog::NULL, maxsize: '10Mb', maxvsize: '10Kb')
56
+ # @param ttl [Integer, nil] lifetime of keys in hours
57
+ # @param cache_min_age [Integer, nil] age which will could be overwritten in cache-control header
58
+ # @raise [ArgumentError] If path is nil/empty, directory doesn't exist, version is nil/empty,
59
+ # or ttl is not nil or not Integer or not positive
60
+ def initialize(path, version, loog: Loog::NULL, maxsize: '10Mb', maxvsize: '10Kb', ttl: nil, cache_min_age: nil)
58
61
  raise ArgumentError, 'Database path cannot be nil or empty' if path.nil? || path.empty?
59
62
  dir = File.dirname(path)
60
63
  raise ArgumentError, "Directory #{dir} does not exist" unless File.directory?(dir)
@@ -64,6 +67,12 @@ class Fbe::Middleware::SqliteStore
64
67
  @loog = loog
65
68
  @maxsize = Filesize.from(maxsize.to_s).to_i
66
69
  @maxvsize = Filesize.from(maxvsize.to_s).to_i
70
+ raise ArgumentError, 'TTL can be nil or Integer > 0' if !ttl.nil? && !(ttl.is_a?(Integer) && ttl.positive?)
71
+ @ttl = ttl
72
+ if !cache_min_age.nil? && !(cache_min_age.is_a?(Integer) && cache_min_age.positive?)
73
+ raise ArgumentError, 'Cache min age can be nil or Integer > 0'
74
+ end
75
+ @cache_min_age = cache_min_age
67
76
  end
68
77
 
69
78
  # Read a value from the cache.
@@ -102,12 +111,32 @@ class Fbe::Middleware::SqliteStore
102
111
  req = JSON.parse(vv[0])
103
112
  req['method'] != 'get'
104
113
  end
114
+ if @cache_min_age && value.is_a?(Array) && value[0].is_a?(Array) && value[0].size > 1
115
+ begin
116
+ resp = JSON.parse(value[0][1])
117
+ rescue TypeError, JSON::ParserError => e
118
+ @loog.info("Failed to parse response to rewrite the cache age: #{e.message}")
119
+ resp = nil
120
+ end
121
+ cache_control = resp.dig('response_headers', 'cache-control') if resp.is_a?(Hash)
122
+ if cache_control && !cache_control.empty?
123
+ %w[max-age s-maxage].each do |key|
124
+ age = cache_control.scan(/#{key}=(\d+)/i).first&.first&.to_i
125
+ if age
126
+ age = [age, @cache_min_age].max
127
+ cache_control = cache_control.sub(/#{key}=(\d+)/, "#{key}=#{age}")
128
+ end
129
+ end
130
+ resp['response_headers']['cache-control'] = cache_control
131
+ value[0][1] = JSON.dump(resp)
132
+ end
133
+ end
105
134
  value = Zlib::Deflate.deflate(JSON.dump(value))
106
135
  return if value.bytesize > @maxvsize
107
136
  perform do |t|
108
137
  t.execute(<<~SQL, [key, value, Time.now.utc.iso8601])
109
- INSERT INTO cache(key, value, touched_at) VALUES(?1, ?2, ?3)
110
- ON CONFLICT(key) DO UPDATE SET value = ?2, touched_at = ?3
138
+ INSERT INTO cache(key, value, touched_at, created_at) VALUES(?1, ?2, ?3, ?3)
139
+ ON CONFLICT(key) DO UPDATE SET value = ?2, touched_at = ?3, created_at = ?3
111
140
  SQL
112
141
  end
113
142
  nil
@@ -153,10 +182,29 @@ class Fbe::Middleware::SqliteStore
153
182
  SQL
154
183
  t.execute 'INSERT INTO cache SELECT * FROM cache_old;'
155
184
  t.execute 'DROP TABLE cache_old;'
185
+ t.execute 'CREATE INDEX IF NOT EXISTS cache_key_idx ON cache(key);'
156
186
  t.execute 'CREATE INDEX IF NOT EXISTS cache_touched_at_idx ON cache(touched_at);'
157
187
  end
158
188
  d.execute 'VACUUM;'
159
189
  end
190
+ if d.execute("SELECT 1 FROM pragma_table_info('cache') WHERE name = 'created_at';").dig(0, 0) != 1
191
+ d.transaction do |t|
192
+ t.execute 'ALTER TABLE cache ADD COLUMN created_at TEXT;'
193
+ t.execute 'UPDATE cache set created_at = ?;', [Time.now.utc.iso8601]
194
+ t.execute 'ALTER TABLE cache RENAME TO cache_old;'
195
+ t.execute <<~SQL
196
+ CREATE TABLE IF NOT EXISTS cache(
197
+ key TEXT UNIQUE NOT NULL, value TEXT, touched_at TEXT NOT NULL, created_at TEXT NOT NULL
198
+ );
199
+ SQL
200
+ t.execute 'INSERT INTO cache SELECT * FROM cache_old;'
201
+ t.execute 'DROP TABLE cache_old;'
202
+ t.execute 'CREATE INDEX IF NOT EXISTS cache_key_idx ON cache(key);'
203
+ t.execute 'CREATE INDEX IF NOT EXISTS cache_touched_at_idx ON cache(touched_at);'
204
+ t.execute 'CREATE INDEX IF NOT EXISTS cache_created_at_idx ON cache(created_at);'
205
+ end
206
+ d.execute 'VACUUM;'
207
+ end
160
208
  found = d.execute("SELECT value FROM meta WHERE key = 'version' LIMIT 1;").dig(0, 0)
161
209
  if found != @version
162
210
  @loog.info("Version mismatch in SQLite cache: stored '#{found}' != current '#{@version}', cleaning up")
@@ -166,6 +214,15 @@ class Fbe::Middleware::SqliteStore
166
214
  end
167
215
  d.execute 'VACUUM;'
168
216
  end
217
+ unless @ttl.nil?
218
+ d.transaction do |t|
219
+ t.execute <<~SQL, [(Time.now.utc - (@ttl * 60 * 60)).iso8601]
220
+ DELETE FROM cache
221
+ WHERE key IN (SELECT key FROM cache WHERE (created_at < ?));
222
+ SQL
223
+ end
224
+ d.execute 'VACUUM;'
225
+ end
169
226
  if File.size(@path) > @maxsize
170
227
  @loog.info(
171
228
  "SQLite cache file size (#{Filesize.from(File.size(@path).to_s).pretty} bytes) exceeds " \
data/lib/fbe/octo.rb CHANGED
@@ -93,7 +93,10 @@ def Fbe.octo(options: $options, global: $global, loog: $loog)
93
93
  if options.sqlite_cache
94
94
  maxsize = Filesize.from(options.sqlite_cache_maxsize || '100M').to_i
95
95
  maxvsize = Filesize.from(options.sqlite_cache_maxvsize || '100K').to_i
96
- store = Fbe::Middleware::SqliteStore.new(options.sqlite_cache, Fbe::VERSION, loog:, maxsize:, maxvsize:)
96
+ cache_min_age = options.sqlite_cache_min_age&.to_i
97
+ store = Fbe::Middleware::SqliteStore.new(
98
+ options.sqlite_cache, Fbe::VERSION, loog:, maxsize:, maxvsize:, ttl: 24, cache_min_age:
99
+ )
97
100
  loog.info(
98
101
  "Using HTTP cache in SQLite file: #{store.path} (" \
99
102
  "#{File.exist?(store.path) ? Filesize.from(File.size(store.path).to_s).pretty : 'file is absent'}, " \
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.24.3' unless const_defined?(:VERSION)
13
+ VERSION = '0.25.0' unless const_defined?(:VERSION)
14
14
  end
@@ -5,10 +5,11 @@
5
5
 
6
6
  require 'faraday'
7
7
  require 'loog'
8
- require_relative '../../test__helper'
8
+ require 'securerandom'
9
9
  require_relative '../../../lib/fbe'
10
10
  require_relative '../../../lib/fbe/middleware'
11
11
  require_relative '../../../lib/fbe/middleware/formatter'
12
+ require_relative '../../test__helper'
12
13
 
13
14
  # Test.
14
15
  # Author:: Yegor Bugayenko (yegor256@gmail.com)
@@ -49,9 +50,38 @@ class LoggingFormatterTest < Fbe::Test
49
50
  end
50
51
  end
51
52
 
53
+ def test_truncate_body_for_error_text_response
54
+ body = SecureRandom.alphanumeric(120)
55
+ log_it(
56
+ status: 502,
57
+ response_body: body,
58
+ response_headers: {
59
+ 'content-type' => 'text/html; charset=utf-8',
60
+ 'x-github-api-version-selected' => '2022-11-28'
61
+ }
62
+ ) do |loog|
63
+ str = loog.to_s
64
+ refute_empty(str)
65
+ [
66
+ %r{http://example.com},
67
+ /Authorization:/,
68
+ /some request body/,
69
+ %r{HTTP/1.1 502},
70
+ /x-github-api-version-selected: "2022-11-28"/,
71
+ %r{content-type: "text/html; charset=utf-8"},
72
+ "#{body.slice(0, 97)}..."
73
+ ].each { |ptn| assert_match(ptn, str) }
74
+ end
75
+ end
76
+
52
77
  private
53
78
 
54
- def log_it(status:, method: :get)
79
+ def log_it(
80
+ status:,
81
+ method: :get,
82
+ response_body: '{"message": "hello, world!"}',
83
+ response_headers: { 'content-type' => 'application/json', 'x-github-api-version-selected' => '2022-11-28' }
84
+ )
55
85
  loog = Loog::Buffer.new
56
86
  formatter = Fbe::Middleware::Formatter.new(logger: loog, options: {})
57
87
  formatter.request(
@@ -67,16 +97,7 @@ class LoggingFormatterTest < Fbe::Test
67
97
  )
68
98
  )
69
99
  formatter.response(
70
- Faraday::Env.from(
71
- {
72
- status:,
73
- response_body: '{"message": "hello, world!"}',
74
- response_headers: {
75
- 'content-type' => 'application/json',
76
- 'x-github-api-version-selected' => '2022-11-28'
77
- }
78
- }
79
- )
100
+ Faraday::Env.from({ status:, response_body:, response_headers: })
80
101
  )
81
102
  yield loog
82
103
  end
@@ -5,10 +5,10 @@
5
5
 
6
6
  require 'faraday'
7
7
  require 'webmock'
8
- require_relative '../../test__helper'
9
8
  require_relative '../../../lib/fbe'
10
9
  require_relative '../../../lib/fbe/middleware'
11
10
  require_relative '../../../lib/fbe/middleware/rate_limit'
11
+ require_relative '../../test__helper'
12
12
 
13
13
  # Test.
14
14
  # Author:: Yegor Bugayenko (yegor256@gmail.com)
@@ -3,11 +3,11 @@
3
3
  # SPDX-FileCopyrightText: Copyright (c) 2024-2025 Zerocracy
4
4
  # SPDX-License-Identifier: MIT
5
5
 
6
- require 'securerandom'
7
6
  require 'qbash'
8
- require_relative '../../test__helper'
7
+ require 'securerandom'
9
8
  require_relative '../../../lib/fbe/middleware'
10
9
  require_relative '../../../lib/fbe/middleware/sqlite_store'
10
+ require_relative '../../test__helper'
11
11
 
12
12
  # Test.
13
13
  # Author:: Yegor Bugayenko (yegor256@gmail.com)
@@ -237,6 +237,187 @@ class SqliteStoreTest < Fbe::Test
237
237
  end
238
238
  end
239
239
 
240
+ def test_upgrade_sqlite_schema_for_add_created_at_column
241
+ with_tmpfile('a.db') do |f|
242
+ SQLite3::Database.new(f).tap do |d|
243
+ d.execute 'CREATE TABLE IF NOT EXISTS cache(key TEXT UNIQUE NOT NULL, value TEXT, touched_at TEXT NOT NULL);'
244
+ [
245
+ ['key1', Zlib::Deflate.deflate(JSON.dump('value1')), Time.now.utc.iso8601],
246
+ ['key2', Zlib::Deflate.deflate(JSON.dump('value2')), Time.now.utc.iso8601]
247
+ ].each { d.execute 'INSERT INTO cache(key, value, touched_at) VALUES(?1, ?2, ?3);', _1 }
248
+ d.execute 'CREATE TABLE IF NOT EXISTS meta(key TEXT UNIQUE NOT NULL, value TEXT);'
249
+ d.execute "INSERT INTO meta(key, value) VALUES('version', ?);", ['0.0.1']
250
+ end
251
+ Fbe::Middleware::SqliteStore.new(f, '0.0.1', loog: fake_loog).then do |store|
252
+ assert_equal('value1', store.read('key1'))
253
+ assert_equal('value2', store.read('key2'))
254
+ rescue SQLite3::SQLException => e
255
+ assert_nil(e)
256
+ end
257
+ end
258
+ end
259
+
260
+ def test_set_correct_ttl
261
+ with_tmpfile('c.db') do |f|
262
+ s = Fbe::Middleware::SqliteStore.new(f, '0.0.1', loog: fake_loog, ttl: nil)
263
+ refute_nil(s)
264
+ s = Fbe::Middleware::SqliteStore.new(f, '0.0.1', loog: fake_loog, ttl: 24)
265
+ refute_nil(s)
266
+ end
267
+ end
268
+
269
+ def test_set_incorrect_ttl
270
+ with_tmpfile('c.db') do |f|
271
+ ex =
272
+ assert_raises(ArgumentError) do
273
+ Fbe::Middleware::SqliteStore.new(f, '0.0.1', loog: fake_loog, ttl: 0)
274
+ end
275
+ assert_equal('TTL can be nil or Integer > 0', ex.message)
276
+ ex =
277
+ assert_raises(ArgumentError) do
278
+ Fbe::Middleware::SqliteStore.new(f, '0.0.1', loog: fake_loog, ttl: -10)
279
+ end
280
+ assert_equal('TTL can be nil or Integer > 0', ex.message)
281
+ ex =
282
+ assert_raises(ArgumentError) do
283
+ Fbe::Middleware::SqliteStore.new(f, '0.0.1', loog: fake_loog, ttl: 10.0)
284
+ end
285
+ assert_equal('TTL can be nil or Integer > 0', ex.message)
286
+ ex =
287
+ assert_raises(ArgumentError) do
288
+ Fbe::Middleware::SqliteStore.new(f, '0.0.1', loog: fake_loog, ttl: '10')
289
+ end
290
+ assert_equal('TTL can be nil or Integer > 0', ex.message)
291
+ ex =
292
+ assert_raises(ArgumentError) do
293
+ Fbe::Middleware::SqliteStore.new(f, '0.0.1', loog: fake_loog, ttl: Object.new)
294
+ end
295
+ assert_equal('TTL can be nil or Integer > 0', ex.message)
296
+ end
297
+ end
298
+
299
+ def test_delete_keys_if_ttl_expired
300
+ with_tmpfile('c.db') do |f|
301
+ now = Time.now
302
+ Time.stub(:now, now) do
303
+ Fbe::Middleware::SqliteStore.new(f, '0.0.1', loog: fake_loog, ttl: 24).then do |s|
304
+ s.write('test1', 'value1')
305
+ s.write('test2', 'value2')
306
+ end
307
+ end
308
+ Time.stub(:now, now + (12 * 60 * 60)) do
309
+ Fbe::Middleware::SqliteStore.new(f, '0.0.1', loog: fake_loog, ttl: 24).then do |s|
310
+ s.write('test3', 'value3')
311
+ s.write('test4', 'value4')
312
+ end
313
+ end
314
+ Time.stub(:now, now + (24 * 60 * 60)) do
315
+ Fbe::Middleware::SqliteStore.new(f, '0.0.1', loog: fake_loog, ttl: 24).then do |s|
316
+ assert_equal('value1', s.read('test1'))
317
+ assert_equal('value2', s.read('test2'))
318
+ assert_equal('value3', s.read('test3'))
319
+ assert_equal('value4', s.read('test4'))
320
+ end
321
+ end
322
+ Time.stub(:now, now + (24 * 60 * 60) + 1) do
323
+ Fbe::Middleware::SqliteStore.new(f, '0.0.1', loog: fake_loog, ttl: 24).then do |s|
324
+ assert_nil(s.read('test1'))
325
+ assert_nil(s.read('test2'))
326
+ assert_equal('value3', s.read('test3'))
327
+ assert_equal('value4', s.read('test4'))
328
+ end
329
+ end
330
+ end
331
+ end
332
+
333
+ def test_set_correct_cache_min_age
334
+ with_tmpfile('c.db') do |f|
335
+ s = Fbe::Middleware::SqliteStore.new(f, '0.0.1', loog: fake_loog, cache_min_age: nil)
336
+ refute_nil(s)
337
+ s = Fbe::Middleware::SqliteStore.new(f, '0.0.1', loog: fake_loog, cache_min_age: 600)
338
+ refute_nil(s)
339
+ end
340
+ end
341
+
342
+ def test_set_incorrect_cache_min_age
343
+ with_tmpfile('c.db') do |f|
344
+ msg = 'Cache min age can be nil or Integer > 0'
345
+ [0, -50, 120.0, '120', Object.new].each do |cache_min_age|
346
+ ex =
347
+ assert_raises(ArgumentError) do
348
+ Fbe::Middleware::SqliteStore.new(f, '0.0.1', loog: fake_loog, cache_min_age:)
349
+ end
350
+ assert_equal(msg, ex.message)
351
+ end
352
+ end
353
+ end
354
+
355
+ def test_not_overwrite_cache_control
356
+ with_tmpfile('t.db') do |f|
357
+ Fbe::Middleware::SqliteStore.new(f, '0.0.1', loog: fake_loog, cache_min_age: 30).then do |store|
358
+ store.write(
359
+ 'test1',
360
+ faraday_value(resp: { 'response_headers' => { 'cache-control' => 'public, max-age=60, s-maxage=60' } })
361
+ )
362
+ store.write(
363
+ 'test2',
364
+ faraday_value(resp: { 'response_headers' => { 'cache-control' => 'public, max-age=30, s-maxage=30' } })
365
+ )
366
+ store.write(
367
+ 'test3',
368
+ faraday_value(resp: { 'response_headers' => { 'content-type' => 'application/json; charset=utf-8' } })
369
+ )
370
+ store.write('test4', faraday_value(resp: { 'status' => 200, 'body' => '{"some":"value"}' }))
371
+ store.write('test5', faraday_value(resp: {}))
372
+ store.write('test6', [[JSON.dump({ 'method' => 'get' }), 1]])
373
+ store.write('test7', faraday_value(resp: 'some string'))
374
+ store.write('test8', faraday_value(resp: nil))
375
+ end
376
+ Fbe::Middleware::SqliteStore.new(f, '0.0.1', loog: fake_loog).then do |store|
377
+ assert_equal(
378
+ 'public, max-age=60, s-maxage=60',
379
+ JSON.parse(store.read('test1')[0][1]).dig('response_headers', 'cache-control')
380
+ )
381
+ assert_equal(
382
+ 'public, max-age=30, s-maxage=30',
383
+ JSON.parse(store.read('test2')[0][1]).dig('response_headers', 'cache-control')
384
+ )
385
+ assert_nil(JSON.parse(store.read('test3')[0][1]).dig('response_headers', 'cache-control'))
386
+ assert_nil(JSON.parse(store.read('test4')[0][1]).dig('response_headers', 'cache-control'))
387
+ assert_nil(JSON.parse(store.read('test5')[0][1]).dig('response_headers', 'cache-control'))
388
+ assert_equal(1, store.read('test6')[0][1])
389
+ assert_equal(JSON.dump('some string'), store.read('test7')[0][1])
390
+ assert_nil(store.read('test8')[0][1])
391
+ end
392
+ end
393
+ end
394
+
395
+ def test_overwrite_cache_control
396
+ with_tmpfile('t.db') do |f|
397
+ Fbe::Middleware::SqliteStore.new(f, '0.0.1', loog: fake_loog, cache_min_age: 300).then do |store|
398
+ store.write(
399
+ 'test1',
400
+ faraday_value(resp: { 'response_headers' => { 'cache-control' => 'public, max-age=60, s-maxage=60' } })
401
+ )
402
+ end
403
+ Fbe::Middleware::SqliteStore.new(f, '0.0.1', loog: fake_loog, cache_min_age: 1555).then do |store|
404
+ store.write(
405
+ 'test2',
406
+ faraday_value(resp: { 'response_headers' => { 'cache-control' => 'public, max-age=60, s-maxage=60' } })
407
+ )
408
+ end
409
+ store = Fbe::Middleware::SqliteStore.new(f, '0.0.1', loog: fake_loog)
410
+ assert_equal(
411
+ 'public, max-age=300, s-maxage=300',
412
+ JSON.parse(store.read('test1')[0][1]).dig('response_headers', 'cache-control')
413
+ )
414
+ assert_equal(
415
+ 'public, max-age=1555, s-maxage=1555',
416
+ JSON.parse(store.read('test2')[0][1]).dig('response_headers', 'cache-control')
417
+ )
418
+ end
419
+ end
420
+
240
421
  private
241
422
 
242
423
  def with_tmpfile(name = 'test.db', &)
@@ -244,4 +425,22 @@ class SqliteStoreTest < Fbe::Test
244
425
  yield File.expand_path(name, dir)
245
426
  end
246
427
  end
428
+
429
+ def faraday_value(
430
+ req: {
431
+ 'method' => 'get',
432
+ 'url' => 'https://example.com/test',
433
+ 'headers' => { 'Content-Type' => 'application/json' }
434
+ },
435
+ resp: {
436
+ 'status' => 200,
437
+ 'body' => '{"some":"value"}',
438
+ 'response_headers' => { 'content-type' => 'application/json; charset=utf-8' }
439
+ }
440
+ )
441
+ value = []
442
+ value << JSON.dump(req) if req
443
+ value << JSON.dump(resp) if resp
444
+ [value]
445
+ end
247
446
  end
@@ -6,10 +6,10 @@
6
6
  require 'faraday'
7
7
  require 'faraday/http_cache'
8
8
  require 'webmock'
9
- require_relative '../../test__helper'
10
9
  require_relative '../../../lib/fbe'
11
10
  require_relative '../../../lib/fbe/middleware'
12
11
  require_relative '../../../lib/fbe/middleware/trace'
12
+ require_relative '../../test__helper'
13
13
 
14
14
  # Test.
15
15
  # Author:: Yegor Bugayenko (yegor256@gmail.com)
@@ -4,10 +4,10 @@
4
4
  # SPDX-License-Identifier: MIT
5
5
 
6
6
  require 'judges/options'
7
- require 'webmock/minitest'
8
7
  require 'loog'
9
- require_relative '../test__helper'
8
+ require 'webmock/minitest'
10
9
  require_relative '../../lib/fbe/github_graph'
10
+ require_relative '../test__helper'
11
11
 
12
12
  # Test.
13
13
  # Author:: Yegor Bugayenko (yegor256@gmail.com)
@@ -3,11 +3,11 @@
3
3
  # SPDX-FileCopyrightText: Copyright (c) 2024-2025 Zerocracy
4
4
  # SPDX-License-Identifier: MIT
5
5
 
6
- require 'tmpdir'
7
6
  require 'factbase'
8
7
  require 'loog'
9
- require_relative '../test__helper'
8
+ require 'tmpdir'
10
9
  require_relative '../../lib/fbe/if_absent'
10
+ require_relative '../test__helper'
11
11
 
12
12
  # Test.
13
13
  # Author:: Yegor Bugayenko (yegor256@gmail.com)
@@ -4,8 +4,8 @@
4
4
  # SPDX-License-Identifier: MIT
5
5
 
6
6
  require 'factbase'
7
- require_relative '../test__helper'
8
7
  require_relative '../../lib/fbe/just_one'
8
+ require_relative '../test__helper'
9
9
 
10
10
  # Test.
11
11
  # Author:: Yegor Bugayenko (yegor256@gmail.com)
@@ -709,4 +709,53 @@ class TestOcto < Fbe::Test
709
709
  end
710
710
  end
711
711
  end
712
+
713
+ def test_octo_with_set_sqlite_cache_min_age
714
+ WebMock.disable_net_connect!
715
+ now = Time.now
716
+ stub_request(:get, 'https://api.github.com/rate_limit')
717
+ .to_return(
718
+ status: 200, headers: { 'Content-Type' => 'application/json', 'X-RateLimit-Remaining' => '5000' },
719
+ body: { 'rate' => { 'limit' => 5000, 'remaining' => 5000, 'reset' => 1_672_531_200 } }.to_json
720
+ )
721
+ Dir.mktmpdir do |dir|
722
+ sqlite_cache = File.expand_path('t.db', dir)
723
+ options = Judges::Options.new({ 'sqlite_cache' => sqlite_cache, 'sqlite_cache_min_age' => 120 })
724
+ o = Fbe.octo(loog: fake_loog, global: {}, options:)
725
+ stub_request(:get, 'https://api.github.com/repositories/798641472').to_return(
726
+ status: 200,
727
+ body: { id: 798_641_472, name: 'factbase' }.to_json,
728
+ headers: {
729
+ 'Date' => now.httpdate,
730
+ 'Content-Type' => 'application/json; charset=utf-8',
731
+ 'Cache-Control' => 'public, max-age=60, s-maxage=60',
732
+ 'Etag' => 'W/"f5f1ea995fd7266816f681aca5a81f539420c469070a47568bebdaa3055487bc"',
733
+ 'Last-Modified' => 'Fri, 04 Jul 2025 13:39:42 GMT'
734
+ }
735
+ ).times(1).then.to_raise('no more request to /repositories/798641472')
736
+ Time.stub(:now, now) do
737
+ assert_equal('factbase', o.repo(798_641_472)['name'])
738
+ end
739
+ Time.stub(:now, now + 50) do
740
+ assert_equal('factbase', o.repo(798_641_472)['name'])
741
+ end
742
+ Time.stub(:now, now + 100) do
743
+ assert_equal('factbase', o.repo(798_641_472)['name'])
744
+ end
745
+ stub_request(:get, 'https://api.github.com/repositories/798641472').to_return(
746
+ status: 200,
747
+ body: { id: 798_641_472, name: 'factbase_changed' }.to_json,
748
+ headers: {
749
+ 'Date' => (now + 120).httpdate,
750
+ 'Content-Type' => 'application/json; charset=utf-8',
751
+ 'Cache-Control' => 'public, max-age=60, s-maxage=60',
752
+ 'Etag' => 'W/"f5f1ea995fd7266816f681aca5a81f539420c469070a47568bebdaa3055487bc"',
753
+ 'Last-Modified' => 'Fri, 04 Jul 2025 13:39:42 GMT'
754
+ }
755
+ )
756
+ Time.stub(:now, now + 120) do
757
+ assert_equal('factbase_changed', o.repo(798_641_472)['name'])
758
+ end
759
+ end
760
+ end
712
761
  end
@@ -5,8 +5,8 @@
5
5
 
6
6
  require 'judges/options'
7
7
  require 'loog'
8
- require_relative '../test__helper'
9
8
  require_relative '../../lib/fbe/unmask_repos'
9
+ require_relative '../test__helper'
10
10
 
11
11
  # Test.
12
12
  # Author:: Yegor Bugayenko (yegor256@gmail.com)
data/test/test__helper.rb CHANGED
@@ -32,8 +32,8 @@ require 'minitest/reporters'
32
32
  Minitest::Reporters.use! [Minitest::Reporters::SpecReporter.new]
33
33
 
34
34
  require 'loog'
35
- require 'minitest/stub_const'
36
35
  require 'minitest/autorun'
36
+ require 'minitest/stub_const'
37
37
  require 'webmock/minitest'
38
38
  require_relative '../lib/fbe'
39
39
 
data/test/test_fbe.rb CHANGED
@@ -3,8 +3,8 @@
3
3
  # SPDX-FileCopyrightText: Copyright (c) 2024-2025 Zerocracy
4
4
  # SPDX-License-Identifier: MIT
5
5
 
6
- require_relative 'test__helper'
7
6
  require_relative '../lib/fbe'
7
+ require_relative 'test__helper'
8
8
 
9
9
  # Main module test.
10
10
  # Author:: Yegor Bugayenko (yegor256@gmail.com)
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.24.3
4
+ version: 0.25.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Yegor Bugayenko