diff --git a/components/ruby/.rubocop.yml b/components/ruby/.rubocop.yml index 8cec8cb4e..cc2978285 100644 --- a/components/ruby/.rubocop.yml +++ b/components/ruby/.rubocop.yml @@ -1,2 +1,4 @@ +# Feel free to correct anything in this file + AllCops: TargetRubyVersion: 3.1 diff --git a/components/ruby/Gemfile.lock b/components/ruby/Gemfile.lock index c2593bcd7..482fb391f 100644 --- a/components/ruby/Gemfile.lock +++ b/components/ruby/Gemfile.lock @@ -1,36 +1,23 @@ PATH remote: . specs: - chef-licensing (1.2.0) - activesupport (~> 7.2, >= 7.2.2.1) + chef-licensing (1.2.1) chef-config (>= 15) faraday (>= 1, < 2) faraday-http-cache + faraday_middleware (~> 1.0) mixlib-log (~> 3.0) ostruct (~> 0.1.0) + pstore (~> 0.1.1) tty-prompt (~> 0.23) tty-spinner (~> 0.9.3) GEM remote: https://rubygems.org/ specs: - activesupport (7.2.2.2) - base64 - benchmark (>= 0.3) - bigdecimal - concurrent-ruby (~> 1.0, >= 1.3.1) - connection_pool (>= 2.2.5) - drb - i18n (>= 1.6, < 2) - logger (>= 1.4.2) - minitest (>= 5.1) - securerandom (>= 0.3) - tzinfo (~> 2.0, >= 2.0.5) addressable (2.8.7) public_suffix (>= 2.0.2, < 7.0) ast (2.4.3) - base64 (0.3.0) - benchmark (0.5.0) bigdecimal (3.3.1) byebug (12.0.0) chef-config (18.8.46) @@ -44,14 +31,12 @@ GEM concurrent-ruby coderay (1.1.3) concurrent-ruby (1.3.5) - connection_pool (2.5.4) cookstyle (8.5.0) rubocop (= 1.81.0) - crack (1.0.1) + crack (1.0.0) bigdecimal rexml diff-lcs (1.6.2) - drb (2.2.3) faraday (1.10.4) faraday-em_http (~> 1.0) faraday-em_synchrony (~> 1.0) @@ -77,23 +62,34 @@ GEM faraday-patron (1.0.0) faraday-rack (1.0.0) faraday-retry (1.0.3) + faraday_middleware (1.2.1) + faraday (~> 1.0) + ffi (1.17.2) + ffi (1.17.2-aarch64-linux-gnu) + ffi (1.17.2-aarch64-linux-musl) + ffi (1.17.2-arm-linux-gnu) + ffi (1.17.2-arm-linux-musl) + ffi (1.17.2-arm64-darwin) ffi (1.17.2-x64-mingw-ucrt) + ffi (1.17.2-x86-linux-gnu) + ffi (1.17.2-x86-linux-musl) + ffi (1.17.2-x86_64-darwin) + ffi (1.17.2-x86_64-linux-gnu) + ffi (1.17.2-x86_64-linux-musl) ffi-win32-extensions (1.0.4) ffi fuzzyurl (0.9.0) hashdiff (1.2.1) - i18n (1.14.7) - concurrent-ruby (~> 1.0) json (2.15.1) language_server-protocol (3.17.0.5) lint_roller (1.1.0) - logger (1.7.0) method_source (1.1.0) - minitest (5.26.0) mixlib-config (3.0.27) tomlrb mixlib-log (3.2.3) ffi (>= 1.15.5) + mixlib-shellout (3.3.9) + chef-utils mixlib-shellout (3.3.9-x64-mingw-ucrt) chef-utils ffi-win32-extensions (~> 1.0.3) @@ -111,17 +107,18 @@ GEM pry (0.15.2) coderay (~> 1.1) method_source (~> 1.0) + pstore (0.1.4) public_suffix (6.0.2) racc (1.8.1) rainbow (3.1.1) rake (13.3.0) regexp_parser (2.11.3) rexml (3.4.4) - rspec (3.13.2) + rspec (3.13.1) rspec-core (~> 3.13.0) rspec-expectations (~> 3.13.0) rspec-mocks (~> 3.13.0) - rspec-core (3.13.6) + rspec-core (3.13.5) rspec-support (~> 3.13.0) rspec-expectations (3.13.5) diff-lcs (>= 1.2.0, < 2.0) @@ -146,7 +143,6 @@ GEM prism (~> 1.4) ruby-progressbar (1.13.0) ruby2_keywords (0.0.5) - securerandom (0.4.1) tomlrb (1.3.0) tty-color (0.6.0) tty-cursor (0.7.1) @@ -160,11 +156,7 @@ GEM tty-screen (0.8.2) tty-spinner (0.9.3) tty-cursor (~> 0.7) - tzinfo (2.0.6) - concurrent-ruby (~> 1.0) - unicode-display_width (3.2.0) - unicode-emoji (~> 4.1) - unicode-emoji (4.1.0) + unicode-display_width (2.6.0) webmock (3.25.1) addressable (>= 2.8.0) crack (>= 0.3.2) @@ -175,7 +167,18 @@ GEM wmi-lite (1.0.7) PLATFORMS + aarch64-linux-gnu + aarch64-linux-musl + arm-linux-gnu + arm-linux-musl + arm64-darwin + ruby x64-mingw-ucrt + x86-linux-gnu + x86-linux-musl + x86_64-darwin + x86_64-linux-gnu + x86_64-linux-musl DEPENDENCIES byebug @@ -187,4 +190,4 @@ DEPENDENCIES webmock (~> 3.25, >= 3.25.1) BUNDLED WITH - 2.3.27 + 2.6.9 diff --git a/components/ruby/chef-licensing.gemspec b/components/ruby/chef-licensing.gemspec index c05045f9a..03940949f 100644 --- a/components/ruby/chef-licensing.gemspec +++ b/components/ruby/chef-licensing.gemspec @@ -30,10 +30,11 @@ Gem::Specification.new do |spec| spec.add_dependency "tty-prompt", "~> 0.23" spec.add_dependency "faraday", ">= 1", "< 2" spec.add_dependency "faraday-http-cache" - spec.add_dependency "activesupport", "~> 7.2", ">= 7.2.2.1" + spec.add_dependency "faraday_middleware", "~> 1.0" spec.add_dependency "tty-spinner", "~> 0.9.3" spec.add_dependency "mixlib-log", "~> 3.0" # Gem dependency needed with Ruby 3.4 upgrade spec.add_dependency "ostruct", "~> 0.1.0" + spec.add_dependency "pstore", "~> 0.1.1" end diff --git a/components/ruby/lib/chef-licensing/list_license_keys.rb b/components/ruby/lib/chef-licensing/list_license_keys.rb index 004d95299..8cee49e64 100644 --- a/components/ruby/lib/chef-licensing/list_license_keys.rb +++ b/components/ruby/lib/chef-licensing/list_license_keys.rb @@ -3,9 +3,11 @@ require_relative "exceptions/describe_error" require "pastel" unless defined?(Pastel) require_relative "config" +require_relative "string_refinements" module ChefLicensing class ListLicenseKeys + using StringRefinements def self.display(opts = {}) new(opts).display end diff --git a/components/ruby/lib/chef-licensing/pstore_adapter.rb b/components/ruby/lib/chef-licensing/pstore_adapter.rb new file mode 100644 index 000000000..655f6ca08 --- /dev/null +++ b/components/ruby/lib/chef-licensing/pstore_adapter.rb @@ -0,0 +1,36 @@ +require "pstore" + +module ChefLicensing + # Simple adapter to make PStore compatible with Faraday::HttpCache + class PStoreAdapter + def initialize(cache_dir) + @store_path = File.join(cache_dir, "chef_licensing_cache.pstore") + @store = PStore.new(@store_path) + end + + # Interface methods required by faraday-http-cache + def read(key) + @store.transaction { @store[key] } + end + + def write(key, value, options = {}) + # PStore handles persistence, Faraday::HttpCache handles + # HTTP cache headers for expiration + @store.transaction { @store[key] = value } + end + + def delete(key) + @store.transaction { @store.delete(key) } + end + + def exist?(key) + @store.transaction { @store.root?(key) } + end + + def clear + @store.transaction do + @store.roots.each { |root| @store.delete(root) } + end + end + end +end diff --git a/components/ruby/lib/chef-licensing/restful_client/base.rb b/components/ruby/lib/chef-licensing/restful_client/base.rb index 2acfe118e..a13625d0f 100644 --- a/components/ruby/lib/chef-licensing/restful_client/base.rb +++ b/components/ruby/lib/chef-licensing/restful_client/base.rb @@ -1,11 +1,12 @@ require "faraday" unless defined?(Faraday) +require "faraday_middleware" require "faraday/http_cache" -require "active_support" require "tmpdir" unless defined?(Dir.mktmpdir) require_relative "../exceptions/restful_client_error" require_relative "../exceptions/restful_client_connection_error" require_relative "../exceptions/missing_api_credentials_error" require_relative "../config" +require_relative "../pstore_adapter" require_relative "middleware/exceptions_handler" require_relative "middleware/content_type_validator" @@ -132,7 +133,7 @@ def handle_post_connection(url = nil) end def get_connection(url = nil) - store = ::ActiveSupport::Cache.lookup_store(:file_store, Dir.tmpdir) + store = PStoreAdapter.new(Dir.tmpdir) Faraday.new(url: url) do |config| config.request :json config.response :json, parser_options: { object_class: OpenStruct } diff --git a/components/ruby/lib/chef-licensing/string_refinements.rb b/components/ruby/lib/chef-licensing/string_refinements.rb new file mode 100644 index 000000000..d8b5a2232 --- /dev/null +++ b/components/ruby/lib/chef-licensing/string_refinements.rb @@ -0,0 +1,25 @@ +module ChefLicensing + # String refinements to provide pluralization functionality + # Replaces ActiveSupport::Inflector for our specific use case + module StringRefinements + refine String do + def pluralize(count = 2) + return self if count == 1 + + # Simple pluralization rules + case downcase + when /s$/, /sh$/, /ch$/, /x$/, /z$/ + "#{self}es" + when /[^aeiou]y$/ + "#{self[0..-2]}ies" + when /f$/ + "#{self[0..-2]}ves" + when /fe$/ + "#{self[0..-3]}ves" + else + "#{self}s" + end + end + end + end +end diff --git a/components/ruby/spec/README.md b/components/ruby/spec/README.md new file mode 100644 index 000000000..1a100a76c --- /dev/null +++ b/components/ruby/spec/README.md @@ -0,0 +1,61 @@ +Test harness notes for the chef-licensing specs + +Purpose +------- +This file documents the decisions made in `spec/spec_helper.rb` to keep the +suite deterministic and hermetic. + +Key points +---------- +- WebMock is required before the library (`require "webmock/rspec"`) so any + HTTP client initialization that happens at require-time is intercepted. + This prevents accidental real HTTP connections during test bootstrap. + +- The tests set a deterministic license server URL via + `ENV["CHEF_LICENSE_SERVER"]` and `ENV["LICENSE_SERVER"]` so tests don't + rely on developer/CI environment values or persisted files that might + point to production servers. + +- `ENV.delete("CHEF_LICENSE_KEY")` is used to ensure tests don't pick up + real credentials from the environment. + +- A suite-level temporary HOME (`TMP_TEST_HOME`) is created early so + require-time code that consults `ENV["HOME"]` or `ENV["USERPROFILE"]` + doesn't see a developer/CI home. Some specs still require a fresh HOME + per-example; the harness provides an `around(:each)` hook that swaps in a + temporary HOME for the duration of those examples. + +- Tests that need HTTP interactions should stub them explicitly using + WebMock's `stub_request`. Consider adding common stubs to + `spec/support/license_server_stubs.rb` to reduce duplication. + +Running the tests +----------------- +From the `components/ruby` directory run: + +```bash +bundle exec rspec --format=documentation +``` + +If you only want to run a single spec file: + +```bash +bundle exec rspec spec/path/to/file_spec.rb +``` + +Adding a new spec +----------------- +- If your spec touches the user's HOME, prefer creating temporary dirs + under `Dir.mktmpdir` and avoid writing into the suite-level HOME + directly. Use the provided per-example HOME swap when you need a + pristine HOME for each example. + +- If your spec makes HTTP calls, add a `stub_request` for the exact + request the code will issue. If multiple specs need the same stubs, + factor them into `spec/support/license_server_stubs.rb` and require that + from `spec/spec_helper.rb`. + +Contact +------- +If you have questions about why this setup exists, check the Git history +for `spec/spec_helper.rb` or ask the maintainer team. \ No newline at end of file diff --git a/components/ruby/spec/chef-licensing/activesupport_replacement_spec.rb b/components/ruby/spec/chef-licensing/activesupport_replacement_spec.rb new file mode 100644 index 000000000..096066e3c --- /dev/null +++ b/components/ruby/spec/chef-licensing/activesupport_replacement_spec.rb @@ -0,0 +1,107 @@ +require "spec_helper" +require "chef-licensing/pstore_adapter" +require "chef-licensing/string_refinements" + +RSpec.describe ChefLicensing::PStoreAdapter do + let(:cache_dir) { Dir.mktmpdir } + let(:adapter) { described_class.new(cache_dir) } + + after do + FileUtils.rm_rf(cache_dir) + end + + describe "#write and #read" do + it "stores and retrieves values" do + adapter.write("test_key", "test_value") + expect(adapter.read("test_key")).to eq("test_value") + end + + it "returns nil for non-existent keys" do + expect(adapter.read("non_existent")).to be_nil + end + end + + describe "#exist?" do + it "returns true for existing keys" do + adapter.write("test_key", "test_value") + expect(adapter.exist?("test_key")).to be true + end + + it "returns false for non-existent keys" do + expect(adapter.exist?("non_existent")).to be false + end + end + + describe "#delete" do + it "removes cached values" do + adapter.write("test_key", "test_value") + adapter.delete("test_key") + expect(adapter.read("test_key")).to be_nil + end + end + + describe "#clear" do + it "removes all cached values" do + adapter.write("key1", "value1") + adapter.write("key2", "value2") + adapter.clear + expect(adapter.read("key1")).to be_nil + expect(adapter.read("key2")).to be_nil + end + end +end + +RSpec.describe ChefLicensing::PStoreAdapter do + let(:cache_dir) { Dir.mktmpdir } + let(:adapter) { described_class.new(cache_dir) } + + after do + FileUtils.rm_rf(cache_dir) + end + + it "provides the same interface as ActiveSupport::Cache" do + adapter.write("test_key", "test_value") + expect(adapter.read("test_key")).to eq("test_value") + expect(adapter.exist?("test_key")).to be true + + adapter.delete("test_key") + expect(adapter.read("test_key")).to be_nil + end +end + +RSpec.describe ChefLicensing::StringRefinements do + using ChefLicensing::StringRefinements + + describe "#pluralize" do + it "pluralizes regular words" do + expect("Day".pluralize(2)).to eq("Days") + expect("cat".pluralize(2)).to eq("cats") + end + + it "handles words ending in s, sh, ch, x, z" do + expect("class".pluralize(2)).to eq("classes") + expect("dish".pluralize(2)).to eq("dishes") + expect("church".pluralize(2)).to eq("churches") + expect("box".pluralize(2)).to eq("boxes") + end + + it "handles words ending in y" do + expect("city".pluralize(2)).to eq("cities") + expect("boy".pluralize(2)).to eq("boys") # vowel before y + end + + it "handles words ending in f/fe" do + expect("leaf".pluralize(2)).to eq("leaves") + expect("knife".pluralize(2)).to eq("knives") + end + + it "returns singular for count of 1" do + expect("Day".pluralize(1)).to eq("Day") + expect("cat".pluralize(1)).to eq("cat") + end + + it "defaults to plural for no argument" do + expect("Day".pluralize).to eq("Days") + end + end +end