From 641df4cc0c7d6316eae98f01fe3864d8d04be6c4 Mon Sep 17 00:00:00 2001 From: Bruno Costanzo Date: Mon, 10 Feb 2025 13:39:54 +0100 Subject: [PATCH 01/10] feature -> cache authenticator --- lib/ruber.rb | 5 +++- lib/ruber/authenticator.rb | 45 +++++++++++++++++++++++++++++ lib/ruber/cache.rb | 25 ++++++++++++++++ lib/ruber/configuration.rb | 15 ++++++++-- test/ruber/authenticator_test.rb | 49 ++++++++++++++++++++++++++++++++ test/ruber/configuration_test.rb | 43 +++++++++++++++++++++++----- test/test_helper.rb | 1 + 7 files changed, 172 insertions(+), 11 deletions(-) create mode 100644 lib/ruber/authenticator.rb create mode 100644 lib/ruber/cache.rb create mode 100644 test/ruber/authenticator_test.rb diff --git a/lib/ruber.rb b/lib/ruber.rb index e6ba8b9..f4d5b9e 100644 --- a/lib/ruber.rb +++ b/lib/ruber.rb @@ -3,11 +3,14 @@ require_relative "ruber/version" require "forwardable" require "ruber/configuration" +require "ruber/authenticator" +require "ruber/cache" # a Ruby wrapper for Uber API module Ruber autoload :Client, "ruber/client" autoload :Error, "ruber/error" + autoload :Authenticator, "ruber/authenticator" DEFAULT_API_BASE = "https://api.uber.com/v1" @@ -16,7 +19,7 @@ class << self def_delegators( :configuration, :customer_id, :client_id, :client_secret, - :customer_id=, :client_id=, :client_secret= + :customer_id=, :client_id=, :client_secret=, :cache_key, :cache_key= ) end end diff --git a/lib/ruber/authenticator.rb b/lib/ruber/authenticator.rb new file mode 100644 index 0000000..3a06f93 --- /dev/null +++ b/lib/ruber/authenticator.rb @@ -0,0 +1,45 @@ +# frozen_string_literal: true + +require "faraday" +require "json" + +module Ruber + class Authenticator + OAUTH_URL = "https://auth.uber.com/oauth/v2/token" + CACHE_KEY = "uber_auth_token" + GRANT_TYPE = "client_credentials" + SCOPE = "eats.deliveries" + + class << self + def access_token + Ruber.cache.read(Ruber.cache_key) || fetch_new_token + end + + def refresh_access_token + Ruber.cache.delete(Ruber.cache_key) + + fetch_new_token + end + + private + + def fetch_new_token + response = Faraday.post( + OAUTH_URL, + { + client_id: Ruber.client_id, + client_secret: Ruber.client_secret, + grant_type: GRANT_TYPE, + scope: SCOPE + } + ) + + data = JSON.parse(response.body.to_s) + + Ruber.cache.write(Ruber.cache_key, data["access_token"], expires_in: data["expires_in"].to_i) + + @access_token = data["access_token"] + end + end + end +end diff --git a/lib/ruber/cache.rb b/lib/ruber/cache.rb new file mode 100644 index 0000000..e420172 --- /dev/null +++ b/lib/ruber/cache.rb @@ -0,0 +1,25 @@ +# frozen_string_literal: true + +module Ruber + class NullCache + def read(key) = memory_store[key] + def write(key, value, _options = {}) = memory_store[key] = value + def clear = memory_store.clear + def delete(key) = memory_store.delete(key) + def memory_store = @memory_store ||= {} + end + + class << self + def cache=(store) + unless %i[read write clear delete].all? { |method| store.respond_to?(method) } + raise ArgumentError, "cache_store must respond to read, write, clear, and delete" + end + + @cache = store + end + + def cache + @cache ||= NullCache.new + end + end +end diff --git a/lib/ruber/configuration.rb b/lib/ruber/configuration.rb index bf4d504..33825b4 100644 --- a/lib/ruber/configuration.rb +++ b/lib/ruber/configuration.rb @@ -3,6 +3,11 @@ module Ruber class Configuration attr_accessor :customer_id, :client_id, :client_secret + attr_writer :cache_key + + def cache_key + @cache_key || "#{customer_id}_#{client_id}_access_token" + end end class << self @@ -10,9 +15,13 @@ def configuration @configuration ||= Configuration.new end - def configuration=(config_hash) - config_hash.each do |key, value| - configuration.send "#{key}=", value + def configuration=(config) + if config.is_a?(Hash) + config.each do |key, value| + configuration.send "#{key}=", value + end + else + @configuration = config end configuration diff --git a/test/ruber/authenticator_test.rb b/test/ruber/authenticator_test.rb new file mode 100644 index 0000000..eaa5fab --- /dev/null +++ b/test/ruber/authenticator_test.rb @@ -0,0 +1,49 @@ +# frozen_string_literal: true + +require "test_helper" +require "webmock/minitest" + +module Ruber + class AuthenticatorTest < Minitest::Test + include WebMock::API + + def setup + Ruber.cache.clear + end + + def test_access_token_fetches_and_caches_token + stub_token_request(access_token: "new_token", expires_in: 3600) + + token = Authenticator.access_token + cached_token = Ruber.cache.read(Ruber.cache_key) + + assert_equal "new_token", token + assert_equal "new_token", cached_token + end + + def test_refresh_access_token_forces_new_token + Ruber.cache.write(Ruber.cache_key, "old_token") + + stub_token_request(access_token: "refreshed_token", expires_in: 3600) + + token = Authenticator.refresh_access_token + cached_token = Ruber.cache.read(Ruber.cache_key) + + assert_equal "refreshed_token", token + assert_equal "refreshed_token", cached_token + end + + private + + def stub_token_request(access_token:, expires_in:) + stub_request(:post, Authenticator::OAUTH_URL) + .to_return( + status: 200, + body: { + access_token: access_token, + expires_in: expires_in + }.to_json + ) + end + end +end diff --git a/test/ruber/configuration_test.rb b/test/ruber/configuration_test.rb index f3f6317..eb7bb6f 100644 --- a/test/ruber/configuration_test.rb +++ b/test/ruber/configuration_test.rb @@ -5,23 +5,32 @@ module Ruber class ConfigurationTest < Minitest::Test def setup - @config_values = { customer_id: "1111", client_id: "2222", client_secret: "a-secret" } + @config_values = { + customer_id: "1111", + client_id: "2222", + client_secret: "a-secret", + cache_key: "custom_cache_key" + } + + Ruber.configuration = nil end def test_configure Ruber.configure do |config| - config.customer_id = @config_values[:customer_id] - config.client_id = @config_values[:client_id] - config.client_secret = @config_values[:client_secret] + config.customer_id = @config_values[:customer_id] + config.client_id = @config_values[:client_id] + config.client_secret = @config_values[:client_secret] + config.cache_key = @config_values[:cache_key] end assert_configuration_values end def test_direct_configuration - Ruber.customer_id = @config_values[:customer_id] - Ruber.client_id = @config_values[:client_id] - Ruber.client_secret = @config_values[:client_secret] + Ruber.customer_id = @config_values[:customer_id] + Ruber.client_id = @config_values[:client_id] + Ruber.client_secret = @config_values[:client_secret] + Ruber.cache_key = @config_values[:cache_key] assert_configuration_values end @@ -38,12 +47,32 @@ def test_invalid_configuration_set end end + def test_cache_key_is_set_by_default + Ruber.configure do |config| + config.customer_id = "1111" + config.client_id = "2222" + end + + assert_equal "1111_2222_access_token", Ruber.cache_key + end + + def test_cache_key_can_be_set_explicitly + Ruber.configure do |config| + config.customer_id = "1111" + config.client_id = "2222" + config.cache_key = @config_values[:cache_key] + end + + assert_equal @config_values[:cache_key], Ruber.cache_key + end + private def assert_configuration_values assert_equal @config_values[:customer_id], Ruber.customer_id assert_equal @config_values[:client_id], Ruber.client_id assert_equal @config_values[:client_secret], Ruber.client_secret + assert_equal @config_values[:cache_key], Ruber.cache_key end end end diff --git a/test/test_helper.rb b/test/test_helper.rb index 84a34ad..312aa10 100644 --- a/test/test_helper.rb +++ b/test/test_helper.rb @@ -4,3 +4,4 @@ require "ruber" require "minitest/autorun" +require "webmock/minitest" From 4cd106a813761276bee7c8e91d780444aa4a84a3 Mon Sep 17 00:00:00 2001 From: Bruno Costanzo Date: Mon, 10 Feb 2025 13:44:57 +0100 Subject: [PATCH 02/10] update readme --- README.md | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/README.md b/README.md index f4dff7c..d5db6f5 100644 --- a/README.md +++ b/README.md @@ -19,6 +19,19 @@ If bundler is not being used to manage dependencies, install the gem by executin ```bash gem install UPDATE_WITH_YOUR_GEM_NAME_IMMEDIATELY_AFTER_RELEASE_TO_RUBYGEMS_ORG ``` +## Cache + +Ruber uses the `Ruber::Cache` class to store the access token. The cache is a simple in-memory cache that is cleared when the access token expires. + +You can configure your own cache implementation by setting the `Ruber.cache` attribute. + +```ruby +Ruber.cache = Redis.new +# or +Ruber.cache = Rails.cache +# or any object that responds to read/write/delete/clear +Ruber.cache = YourCustomCache.new +``` ## Usage From 80568f577ee5cd528c2cfa943c1fa9ce63f6b3e2 Mon Sep 17 00:00:00 2001 From: Bruno Costanzo Date: Mon, 10 Feb 2025 14:02:13 +0100 Subject: [PATCH 03/10] translate cache to configuration --- lib/ruber.rb | 5 ++--- lib/ruber/configuration.rb | 14 ++++++++++++++ lib/ruber/configuration/null_cache.rb | 13 +++++++++++++ test/ruber/configuration_test.rb | 20 ++++++++++++++++++++ 4 files changed, 49 insertions(+), 3 deletions(-) create mode 100644 lib/ruber/configuration/null_cache.rb diff --git a/lib/ruber.rb b/lib/ruber.rb index f4d5b9e..6eb85c8 100644 --- a/lib/ruber.rb +++ b/lib/ruber.rb @@ -4,7 +4,6 @@ require "forwardable" require "ruber/configuration" require "ruber/authenticator" -require "ruber/cache" # a Ruby wrapper for Uber API module Ruber @@ -18,8 +17,8 @@ class << self extend Forwardable def_delegators( - :configuration, :customer_id, :client_id, :client_secret, - :customer_id=, :client_id=, :client_secret=, :cache_key, :cache_key= + :configuration, :customer_id, :client_id, :client_secret, :cache, + :customer_id=, :client_id=, :client_secret=, :cache_key, :cache_key=, :cache= ) end end diff --git a/lib/ruber/configuration.rb b/lib/ruber/configuration.rb index 33825b4..e0dde06 100644 --- a/lib/ruber/configuration.rb +++ b/lib/ruber/configuration.rb @@ -1,10 +1,24 @@ # frozen_string_literal: true +require_relative "configuration/null_cache" + module Ruber class Configuration attr_accessor :customer_id, :client_id, :client_secret attr_writer :cache_key + def cache + @cache ||= NullCache.new + end + + def cache=(store) + unless %i[read write clear delete].all? { |method| store.respond_to?(method) } + raise ArgumentError, "cache_store must respond to read, write, clear, and delete" + end + + @cache = store + end + def cache_key @cache_key || "#{customer_id}_#{client_id}_access_token" end diff --git a/lib/ruber/configuration/null_cache.rb b/lib/ruber/configuration/null_cache.rb new file mode 100644 index 0000000..90a31c5 --- /dev/null +++ b/lib/ruber/configuration/null_cache.rb @@ -0,0 +1,13 @@ +# frozen_string_literal: true + +module Ruber + class Configuration + class NullCache + def read(key) = memory_store[key] + def write(key, value, _options = {}) = memory_store[key] = value + def clear = memory_store.clear + def delete(key) = memory_store.delete(key) + def memory_store = @memory_store ||= {} + end + end +end diff --git a/test/ruber/configuration_test.rb b/test/ruber/configuration_test.rb index eb7bb6f..180332f 100644 --- a/test/ruber/configuration_test.rb +++ b/test/ruber/configuration_test.rb @@ -12,6 +12,14 @@ def setup cache_key: "custom_cache_key" } + @custom_cache = Class.new do + def read(key) = memory_store[key] + def write(key, value, _options = {}) = memory_store[key] = value + def clear = memory_store.clear + def delete(key) = memory_store.delete(key) + def memory_store = @memory_store ||= {} + end.new + Ruber.configuration = nil end @@ -66,6 +74,18 @@ def test_cache_key_can_be_set_explicitly assert_equal @config_values[:cache_key], Ruber.cache_key end + def test_cache_can_be_set_explicitly + Ruber.cache = @custom_cache + + assert_equal @custom_cache, Ruber.cache + end + + def test_cache_must_respond_to_read_write_clear_and_delete + assert_raises(ArgumentError) do + Ruber.cache = Object.new + end + end + private def assert_configuration_values From 3a815d234a6a60ceb4580257f104cd71eb75e79a Mon Sep 17 00:00:00 2001 From: Bruno Costanzo Date: Mon, 10 Feb 2025 14:22:16 +0100 Subject: [PATCH 04/10] code suggestions --- Gemfile | 4 ++++ Gemfile.lock | 14 ++++++++++++++ README.md | 4 +--- lib/ruber/authenticator.rb | 1 - 4 files changed, 19 insertions(+), 4 deletions(-) diff --git a/Gemfile b/Gemfile index a05f80f..7688bdd 100644 --- a/Gemfile +++ b/Gemfile @@ -14,3 +14,7 @@ gem "rubocop", "~> 1.21" group :development do gem "dotenv" end + +group :test do + gem "webmock" +end diff --git a/Gemfile.lock b/Gemfile.lock index 1596d66..fe2df17 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -8,7 +8,13 @@ PATH GEM remote: https://rubygems.org/ specs: + addressable (2.8.7) + public_suffix (>= 2.0.2, < 7.0) ast (2.4.2) + bigdecimal (3.1.9) + crack (1.0.0) + bigdecimal + rexml dotenv (3.1.7) faraday (1.10.4) faraday-em_http (~> 1.0) @@ -35,6 +41,7 @@ GEM faraday-retry (1.0.3) faraday_middleware (1.2.1) faraday (~> 1.0) + hashdiff (1.1.2) json (2.9.1) language_server-protocol (3.17.0.4) minitest (5.25.4) @@ -43,10 +50,12 @@ GEM parser (3.3.7.0) ast (~> 2.4.1) racc + public_suffix (6.0.1) racc (1.8.1) rainbow (3.1.1) rake (13.2.1) regexp_parser (2.10.0) + rexml (3.4.0) rubocop (1.71.0) json (~> 2.3) language_server-protocol (>= 3.17.0) @@ -64,6 +73,10 @@ GEM unicode-display_width (3.1.4) unicode-emoji (~> 4.0, >= 4.0.4) unicode-emoji (4.0.4) + webmock (3.25.0) + addressable (>= 2.8.0) + crack (>= 0.3.2) + hashdiff (>= 0.4.0, < 2.0.0) PLATFORMS ruby @@ -75,6 +88,7 @@ DEPENDENCIES rake (~> 13.0) ruber! rubocop (~> 1.21) + webmock BUNDLED WITH 2.5.22 diff --git a/README.md b/README.md index d5db6f5..a2d3e57 100644 --- a/README.md +++ b/README.md @@ -20,10 +20,8 @@ If bundler is not being used to manage dependencies, install the gem by executin gem install UPDATE_WITH_YOUR_GEM_NAME_IMMEDIATELY_AFTER_RELEASE_TO_RUBYGEMS_ORG ``` ## Cache +Ruber uses a caching solution to improve efficiency (e.g., for caching tokens). By default, it uses a simple in-memory cache, but you can change the cache method by setting the `Ruber.cache` attribute -Ruber uses the `Ruber::Cache` class to store the access token. The cache is a simple in-memory cache that is cleared when the access token expires. - -You can configure your own cache implementation by setting the `Ruber.cache` attribute. ```ruby Ruber.cache = Redis.new diff --git a/lib/ruber/authenticator.rb b/lib/ruber/authenticator.rb index 3a06f93..5671134 100644 --- a/lib/ruber/authenticator.rb +++ b/lib/ruber/authenticator.rb @@ -6,7 +6,6 @@ module Ruber class Authenticator OAUTH_URL = "https://auth.uber.com/oauth/v2/token" - CACHE_KEY = "uber_auth_token" GRANT_TYPE = "client_credentials" SCOPE = "eats.deliveries" From c6ebd4b34bb8c9a057b27bdf6007ae1fb3f070d8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nicol=C3=A1s=20Gald=C3=A1mez?= Date: Mon, 10 Feb 2025 16:21:10 -0300 Subject: [PATCH 05/10] Move cache_key to authenticator --- lib/ruber.rb | 4 ++-- lib/ruber/authenticator.rb | 10 +++++++--- lib/ruber/configuration.rb | 5 ----- test/ruber/authenticator_test.rb | 6 +++--- test/ruber/configuration_test.rb | 25 +------------------------ 5 files changed, 13 insertions(+), 37 deletions(-) diff --git a/lib/ruber.rb b/lib/ruber.rb index 6eb85c8..24282f7 100644 --- a/lib/ruber.rb +++ b/lib/ruber.rb @@ -17,8 +17,8 @@ class << self extend Forwardable def_delegators( - :configuration, :customer_id, :client_id, :client_secret, :cache, - :customer_id=, :client_id=, :client_secret=, :cache_key, :cache_key=, :cache= + :configuration, :customer_id, :client_id, :client_secret, + :customer_id=, :client_id=, :client_secret=, :cache, :cache= ) end end diff --git a/lib/ruber/authenticator.rb b/lib/ruber/authenticator.rb index 5671134..644aef4 100644 --- a/lib/ruber/authenticator.rb +++ b/lib/ruber/authenticator.rb @@ -11,15 +11,19 @@ class Authenticator class << self def access_token - Ruber.cache.read(Ruber.cache_key) || fetch_new_token + Ruber.cache.read(cache_key) || fetch_new_token end def refresh_access_token - Ruber.cache.delete(Ruber.cache_key) + Ruber.cache.delete(cache_key) fetch_new_token end + def cache_key + @cache_key ||= "#{Ruber.customer_id}_#{Ruber.client_id}_access_token" + end + private def fetch_new_token @@ -35,7 +39,7 @@ def fetch_new_token data = JSON.parse(response.body.to_s) - Ruber.cache.write(Ruber.cache_key, data["access_token"], expires_in: data["expires_in"].to_i) + Ruber.cache.write(cache_key, data["access_token"], expires_in: data["expires_in"].to_i) @access_token = data["access_token"] end diff --git a/lib/ruber/configuration.rb b/lib/ruber/configuration.rb index e0dde06..768ec6e 100644 --- a/lib/ruber/configuration.rb +++ b/lib/ruber/configuration.rb @@ -5,7 +5,6 @@ module Ruber class Configuration attr_accessor :customer_id, :client_id, :client_secret - attr_writer :cache_key def cache @cache ||= NullCache.new @@ -18,10 +17,6 @@ def cache=(store) @cache = store end - - def cache_key - @cache_key || "#{customer_id}_#{client_id}_access_token" - end end class << self diff --git a/test/ruber/authenticator_test.rb b/test/ruber/authenticator_test.rb index eaa5fab..9c1651a 100644 --- a/test/ruber/authenticator_test.rb +++ b/test/ruber/authenticator_test.rb @@ -15,19 +15,19 @@ def test_access_token_fetches_and_caches_token stub_token_request(access_token: "new_token", expires_in: 3600) token = Authenticator.access_token - cached_token = Ruber.cache.read(Ruber.cache_key) + cached_token = Ruber.cache.read(Authenticator.cache_key) assert_equal "new_token", token assert_equal "new_token", cached_token end def test_refresh_access_token_forces_new_token - Ruber.cache.write(Ruber.cache_key, "old_token") + Ruber.cache.write(Authenticator.cache_key, "old_token") stub_token_request(access_token: "refreshed_token", expires_in: 3600) token = Authenticator.refresh_access_token - cached_token = Ruber.cache.read(Ruber.cache_key) + cached_token = Ruber.cache.read(Authenticator.cache_key) assert_equal "refreshed_token", token assert_equal "refreshed_token", cached_token diff --git a/test/ruber/configuration_test.rb b/test/ruber/configuration_test.rb index 180332f..1033a92 100644 --- a/test/ruber/configuration_test.rb +++ b/test/ruber/configuration_test.rb @@ -8,8 +8,7 @@ def setup @config_values = { customer_id: "1111", client_id: "2222", - client_secret: "a-secret", - cache_key: "custom_cache_key" + client_secret: "a-secret" } @custom_cache = Class.new do @@ -28,7 +27,6 @@ def test_configure config.customer_id = @config_values[:customer_id] config.client_id = @config_values[:client_id] config.client_secret = @config_values[:client_secret] - config.cache_key = @config_values[:cache_key] end assert_configuration_values @@ -38,7 +36,6 @@ def test_direct_configuration Ruber.customer_id = @config_values[:customer_id] Ruber.client_id = @config_values[:client_id] Ruber.client_secret = @config_values[:client_secret] - Ruber.cache_key = @config_values[:cache_key] assert_configuration_values end @@ -55,25 +52,6 @@ def test_invalid_configuration_set end end - def test_cache_key_is_set_by_default - Ruber.configure do |config| - config.customer_id = "1111" - config.client_id = "2222" - end - - assert_equal "1111_2222_access_token", Ruber.cache_key - end - - def test_cache_key_can_be_set_explicitly - Ruber.configure do |config| - config.customer_id = "1111" - config.client_id = "2222" - config.cache_key = @config_values[:cache_key] - end - - assert_equal @config_values[:cache_key], Ruber.cache_key - end - def test_cache_can_be_set_explicitly Ruber.cache = @custom_cache @@ -92,7 +70,6 @@ def assert_configuration_values assert_equal @config_values[:customer_id], Ruber.customer_id assert_equal @config_values[:client_id], Ruber.client_id assert_equal @config_values[:client_secret], Ruber.client_secret - assert_equal @config_values[:cache_key], Ruber.cache_key end end end From 14df933533d811631bb7d51a1974a7f20774f051 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nicol=C3=A1s=20Gald=C3=A1mez?= Date: Mon, 10 Feb 2025 16:50:23 -0300 Subject: [PATCH 06/10] Refresh token if expired --- lib/ruber/authenticator.rb | 19 ++++++++++++++++--- test/ruber/authenticator_test.rb | 18 +++++++++++++++--- 2 files changed, 31 insertions(+), 6 deletions(-) diff --git a/lib/ruber/authenticator.rb b/lib/ruber/authenticator.rb index 644aef4..ce93838 100644 --- a/lib/ruber/authenticator.rb +++ b/lib/ruber/authenticator.rb @@ -11,7 +11,11 @@ class Authenticator class << self def access_token - Ruber.cache.read(cache_key) || fetch_new_token + @access_token = cached_token || fetch_new_token + + @access_token = refresh_access_token if token_expired? + + @access_token end def refresh_access_token @@ -26,6 +30,14 @@ def cache_key private + def token_expired? + cached_token[:expires_at] < Time.now + end + + def cached_token + Ruber.cache.read(cache_key) + end + def fetch_new_token response = Faraday.post( OAUTH_URL, @@ -39,9 +51,10 @@ def fetch_new_token data = JSON.parse(response.body.to_s) - Ruber.cache.write(cache_key, data["access_token"], expires_in: data["expires_in"].to_i) + expires_at = Time.now + data["expires_in"].to_i + Ruber.cache.write(cache_key, { token: data["access_token"], expires_at: expires_at }) - @access_token = data["access_token"] + data["access_token"] end end end diff --git a/test/ruber/authenticator_test.rb b/test/ruber/authenticator_test.rb index 9c1651a..a330617 100644 --- a/test/ruber/authenticator_test.rb +++ b/test/ruber/authenticator_test.rb @@ -18,11 +18,11 @@ def test_access_token_fetches_and_caches_token cached_token = Ruber.cache.read(Authenticator.cache_key) assert_equal "new_token", token - assert_equal "new_token", cached_token + assert_equal "new_token", cached_token[:token] end def test_refresh_access_token_forces_new_token - Ruber.cache.write(Authenticator.cache_key, "old_token") + Ruber.cache.write(Authenticator.cache_key, { token: "old_token", expires_at: Time.now + 3600 }) stub_token_request(access_token: "refreshed_token", expires_in: 3600) @@ -30,7 +30,19 @@ def test_refresh_access_token_forces_new_token cached_token = Ruber.cache.read(Authenticator.cache_key) assert_equal "refreshed_token", token - assert_equal "refreshed_token", cached_token + assert_equal "refreshed_token", cached_token[:token] + end + + def test_refresh_access_token_if_expired + Ruber.cache.write(Authenticator.cache_key, { token: "expired_token", expires_at: Time.now - 3600 }) + + stub_token_request(access_token: "refreshed_token", expires_in: 3600) + + token = Authenticator.access_token + cached_token = Ruber.cache.read(Authenticator.cache_key) + + assert_equal "refreshed_token", token + assert_equal "refreshed_token", cached_token[:token] end private From f76ec29fe58fa3025e0e2d5981c81e7930872a78 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nicol=C3=A1s=20Gald=C3=A1mez?= Date: Tue, 11 Feb 2025 09:22:39 -0300 Subject: [PATCH 07/10] File cache store --- lib/ruber/configuration.rb | 5 ++- lib/ruber/configuration/file_cache.rb | 18 ++++++++ test/ruber/configuration/file_cache_test.rb | 48 +++++++++++++++++++++ test/test_helper.rb | 8 ++++ 4 files changed, 77 insertions(+), 2 deletions(-) create mode 100644 lib/ruber/configuration/file_cache.rb create mode 100644 test/ruber/configuration/file_cache_test.rb diff --git a/lib/ruber/configuration.rb b/lib/ruber/configuration.rb index 768ec6e..c4661a7 100644 --- a/lib/ruber/configuration.rb +++ b/lib/ruber/configuration.rb @@ -1,13 +1,14 @@ # frozen_string_literal: true require_relative "configuration/null_cache" +require_relative "configuration/file_cache" module Ruber class Configuration - attr_accessor :customer_id, :client_id, :client_secret + attr_accessor :customer_id, :client_id, :client_secret, :file_cache_path def cache - @cache ||= NullCache.new + @cache ||= FileCache.new end def cache=(store) diff --git a/lib/ruber/configuration/file_cache.rb b/lib/ruber/configuration/file_cache.rb new file mode 100644 index 0000000..dfec44c --- /dev/null +++ b/lib/ruber/configuration/file_cache.rb @@ -0,0 +1,18 @@ +# frozen_string_literal: true + +require "yaml/store" + +module Ruber + class FileCache + def initialize + raise "FileCache requires a file path" unless Ruber.configuration.file_cache_path + + @store = YAML::Store.new(Ruber.configuration.file_cache_path) + end + + def read(key) = @store.transaction { @store[key] } + def write(key, value) = @store.transaction { @store[key] = value } + def clear = File.delete(Ruber.configuration.file_cache_path) if File.exist?(Ruber.configuration.file_cache_path) + def delete(key) = @store.transaction { @store.delete(key) } + end +end diff --git a/test/ruber/configuration/file_cache_test.rb b/test/ruber/configuration/file_cache_test.rb new file mode 100644 index 0000000..3a6a32d --- /dev/null +++ b/test/ruber/configuration/file_cache_test.rb @@ -0,0 +1,48 @@ +# frozen_string_literal: true + +require File.expand_path("../../test_helper", __dir__) +require "yaml/store" +require "fileutils" + +module Ruber + class FileCacheTest < Minitest::Test + def setup + Ruber.configuration.file_cache_path = File.join(Dir.tmpdir, "file_cache_test.yaml") + File.delete(Ruber.configuration.file_cache_path) if File.exist?(Ruber.configuration.file_cache_path) + + @cache = FileCache.new + end + + def teardown + File.delete(Ruber.configuration.file_cache_path) if File.exist?(Ruber.configuration.file_cache_path) + end + + def test_initialize_raises_error_if_no_file_path + Ruber.configuration.stub :file_cache_path, nil do + assert_raises(RuntimeError, "FileCache requires a file path") do + FileCache.new + end + end + end + + def test_write_and_read + @cache.write("foo", "bar") + assert_equal "bar", @cache.read("foo") + end + + def test_delete + @cache.write("key", "value") + @cache.delete("key") + assert_nil @cache.read("key") + end + + def test_clear + @cache.write("key1", "value1") + @cache.write("key2", "value2") + + @cache.clear + assert_nil @cache.read("key1") + assert_nil @cache.read("key2") + end + end +end diff --git a/test/test_helper.rb b/test/test_helper.rb index 312aa10..cab85d2 100644 --- a/test/test_helper.rb +++ b/test/test_helper.rb @@ -5,3 +5,11 @@ require "minitest/autorun" require "webmock/minitest" + +class Minitest::Test # rubocop:disable Style/ClassAndModuleChildren + def setup + Ruber.configuration.file_cache_path ||= File.join(Dir.tmpdir, "file_cache_test.yaml") + + super + end +end From e6c941ff32cdb0e609a089a9c9e80898d4961b2b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nicol=C3=A1s=20Gald=C3=A1mez?= Date: Tue, 11 Feb 2025 16:46:15 -0300 Subject: [PATCH 08/10] Assign file_cache_path on before_setup --- .gitignore | 1 + lib/ruber/configuration/file_cache.rb | 6 +++++- test/ruber/authenticator_test.rb | 2 +- test/ruber/configuration/file_cache_test.rb | 1 - test/test_helper.rb | 9 +++++---- 5 files changed, 12 insertions(+), 7 deletions(-) diff --git a/.gitignore b/.gitignore index 9106b2a..ac51ff7 100644 --- a/.gitignore +++ b/.gitignore @@ -6,3 +6,4 @@ /pkg/ /spec/reports/ /tmp/ +file_cache_test.yaml diff --git a/lib/ruber/configuration/file_cache.rb b/lib/ruber/configuration/file_cache.rb index dfec44c..9de0a59 100644 --- a/lib/ruber/configuration/file_cache.rb +++ b/lib/ruber/configuration/file_cache.rb @@ -12,7 +12,11 @@ def initialize def read(key) = @store.transaction { @store[key] } def write(key, value) = @store.transaction { @store[key] = value } - def clear = File.delete(Ruber.configuration.file_cache_path) if File.exist?(Ruber.configuration.file_cache_path) + + def clear + File.delete(Ruber.configuration.file_cache_path) if File.exist?(Ruber.configuration.file_cache_path) + end + def delete(key) = @store.transaction { @store.delete(key) } end end diff --git a/test/ruber/authenticator_test.rb b/test/ruber/authenticator_test.rb index a330617..a4730ef 100644 --- a/test/ruber/authenticator_test.rb +++ b/test/ruber/authenticator_test.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -require "test_helper" +require File.expand_path("../test_helper", __dir__) require "webmock/minitest" module Ruber diff --git a/test/ruber/configuration/file_cache_test.rb b/test/ruber/configuration/file_cache_test.rb index 3a6a32d..1e8d212 100644 --- a/test/ruber/configuration/file_cache_test.rb +++ b/test/ruber/configuration/file_cache_test.rb @@ -7,7 +7,6 @@ module Ruber class FileCacheTest < Minitest::Test def setup - Ruber.configuration.file_cache_path = File.join(Dir.tmpdir, "file_cache_test.yaml") File.delete(Ruber.configuration.file_cache_path) if File.exist?(Ruber.configuration.file_cache_path) @cache = FileCache.new diff --git a/test/test_helper.rb b/test/test_helper.rb index cab85d2..f4149ef 100644 --- a/test/test_helper.rb +++ b/test/test_helper.rb @@ -1,15 +1,16 @@ # frozen_string_literal: true $LOAD_PATH.unshift File.expand_path("../lib", __dir__) -require "ruber" require "minitest/autorun" require "webmock/minitest" -class Minitest::Test # rubocop:disable Style/ClassAndModuleChildren - def setup - Ruber.configuration.file_cache_path ||= File.join(Dir.tmpdir, "file_cache_test.yaml") +require "ruber" +class Minitest::Test # rubocop:disable Style/ClassAndModuleChildren + def before_setup super + + Ruber.configuration.file_cache_path = "file_cache_test.yaml" end end From 613d21fa77213b293b57b73af4d18288e947e030 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nicol=C3=A1s=20Gald=C3=A1mez?= Date: Wed, 12 Feb 2025 07:30:25 -0300 Subject: [PATCH 09/10] Add complex object test to file cache --- test/ruber/configuration/file_cache_test.rb | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/test/ruber/configuration/file_cache_test.rb b/test/ruber/configuration/file_cache_test.rb index 1e8d212..a25727d 100644 --- a/test/ruber/configuration/file_cache_test.rb +++ b/test/ruber/configuration/file_cache_test.rb @@ -29,6 +29,12 @@ def test_write_and_read assert_equal "bar", @cache.read("foo") end + def test_write_and_read_complex_object + @cache.write("foo", { foo: "bar", bar: "baz" }) + assert_equal "bar", @cache.read("foo")[:foo] + assert_equal "baz", @cache.read("foo")[:bar] + end + def test_delete @cache.write("key", "value") @cache.delete("key") From 6fb83b83ea3cbd0c1f3cdedaa86cc49f6b54456c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nicol=C3=A1s=20Gald=C3=A1mez?= Date: Wed, 12 Feb 2025 09:55:07 -0300 Subject: [PATCH 10/10] Changes based on PR comments --- test/ruber/configuration/file_cache_test.rb | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/test/ruber/configuration/file_cache_test.rb b/test/ruber/configuration/file_cache_test.rb index a25727d..2c5e0b2 100644 --- a/test/ruber/configuration/file_cache_test.rb +++ b/test/ruber/configuration/file_cache_test.rb @@ -13,7 +13,7 @@ def setup end def teardown - File.delete(Ruber.configuration.file_cache_path) if File.exist?(Ruber.configuration.file_cache_path) + @cache.clear end def test_initialize_raises_error_if_no_file_path @@ -49,5 +49,15 @@ def test_clear assert_nil @cache.read("key1") assert_nil @cache.read("key2") end + + def test_clear_and_write_again + @cache.write("key1", "value1") + @cache.write("key2", "value1") + @cache.clear + @cache.write("key1", "value2") + + assert "value2", @cache.read("key1") + assert_nil @cache.read("key2") + end end end