diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 03ca12e..7f7241a 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -5,23 +5,13 @@ jobs: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v1 + - uses: actions/checkout@v4 - name: Set up Ruby uses: ruby/setup-ruby@v1 with: - ruby-version: 2.7.8 - rubygems: latest - - - name: Cache gem dependencies - uses: actions/cache@v1 - with: - path: vendor/bundle - key: ${{ runner.os }}-bundler-${{ hashFiles('**/Gemfile.lock') }} - restore-keys: ${{ runner.os }}-bundler- - - - name: Install dependencies - run: gem install bundler && bundle install --jobs 4 --retry 3 --path vendor/bundle + ruby-version: 3.2 + bundler-cache: true - name: Run tests run: bundle exec rake test diff --git a/Gemfile.lock b/Gemfile.lock index d9df3cd..aa887a1 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,83 +1,138 @@ GIT remote: https://github.com/basecamp/house-style.git - revision: b3ad65254828e8e8019a0d9a6205aff9ad206a77 + revision: 6f73ca5c3fd5002b48aeede2d980f9b8fe047d55 branch: main specs: - rubocop-37signals (1.0.0) - rubocop - rubocop-minitest - rubocop-performance - rubocop-rails + rubocop-37signals (1.2.1) + rubocop (>= 1.72) + rubocop-minitest (>= 0.37.0) + rubocop-performance (>= 1.24) + rubocop-rails (>= 2.30) PATH remote: . specs: rspamd-ruby (1.0.0) + activesupport (>= 6.0) GEM remote: https://rubygems.org/ specs: - activesupport (7.0.7.2) - concurrent-ruby (~> 1.0, >= 1.0.2) + activesupport (8.1.2) + base64 + bigdecimal + concurrent-ruby (~> 1.0, >= 1.3.1) + connection_pool (>= 2.2.5) + drb i18n (>= 1.6, < 2) + json + logger (>= 1.4.2) minitest (>= 5.1) - tzinfo (~> 2.0) - addressable (2.8.1) - public_suffix (>= 2.0.2, < 6.0) - ast (2.4.2) - concurrent-ruby (1.2.2) - crack (0.4.3) - safe_yaml (~> 1.0.0) - debug (1.7.1) - irb (>= 1.5.0) - reline (>= 0.3.1) - hashdiff (1.0.0) - i18n (1.14.1) + securerandom (>= 0.3) + tzinfo (~> 2.0, >= 2.0.5) + uri (>= 0.13.1) + addressable (2.8.9) + public_suffix (>= 2.0.2, < 8.0) + ast (2.4.3) + base64 (0.3.0) + bigdecimal (4.0.1) + concurrent-ruby (1.3.6) + connection_pool (3.0.2) + crack (1.0.1) + bigdecimal + rexml + date (3.5.1) + debug (1.11.1) + irb (~> 1.10) + reline (>= 0.3.8) + drb (2.2.3) + erb (6.0.2) + hashdiff (1.2.1) + i18n (1.14.8) concurrent-ruby (~> 1.0) - io-console (0.6.0) - irb (1.6.2) - reline (>= 0.3.0) - json (2.6.3) - minitest (5.17.0) - parallel (1.22.1) - parser (3.2.1.0) + io-console (0.8.2) + irb (1.17.0) + pp (>= 0.6.0) + prism (>= 1.3.0) + rdoc (>= 4.0.0) + reline (>= 0.4.2) + json (2.18.1) + json-schema (6.1.0) + addressable (~> 2.8) + bigdecimal (>= 3.1, < 5) + language_server-protocol (3.17.0.5) + lint_roller (1.1.0) + logger (1.7.0) + mcp (0.8.0) + json-schema (>= 4.1) + minitest (6.0.2) + drb (~> 2.0) + prism (~> 1.5) + parallel (1.27.0) + parser (3.3.10.2) ast (~> 2.4.1) - public_suffix (5.0.0) - rack (3.0.8) + racc + pp (0.6.3) + prettyprint + prettyprint (0.2.0) + prism (1.9.0) + psych (5.3.1) + date + stringio + public_suffix (7.0.5) + racc (1.8.1) + rack (3.2.5) rainbow (3.1.1) - rake (13.0.1) - regexp_parser (2.7.0) - reline (0.3.2) + rake (13.3.1) + rdoc (7.2.0) + erb + psych (>= 4.0.0) + tsort + regexp_parser (2.11.3) + reline (0.6.3) io-console (~> 0.5) - rexml (3.2.5) - rubocop (1.45.1) + rexml (3.4.4) + rubocop (1.85.1) json (~> 2.3) + language_server-protocol (~> 3.17.0.2) + lint_roller (~> 1.1.0) + mcp (~> 0.6) parallel (~> 1.10) - parser (>= 3.2.0.0) + parser (>= 3.3.0.2) rainbow (>= 2.2.2, < 4.0) - regexp_parser (>= 1.8, < 3.0) - rexml (>= 3.2.5, < 4.0) - rubocop-ast (>= 1.24.1, < 2.0) + regexp_parser (>= 2.9.3, < 3.0) + rubocop-ast (>= 1.49.0, < 2.0) ruby-progressbar (~> 1.7) - unicode-display_width (>= 2.4.0, < 3.0) - rubocop-ast (1.26.0) - parser (>= 3.2.1.0) - rubocop-minitest (0.27.0) - rubocop (>= 0.90, < 2.0) - rubocop-performance (1.16.0) - rubocop (>= 1.7.0, < 2.0) - rubocop-ast (>= 0.4.0) - rubocop-rails (2.19.1) + unicode-display_width (>= 2.4.0, < 4.0) + rubocop-ast (1.49.0) + parser (>= 3.3.7.2) + prism (~> 1.7) + rubocop-minitest (0.39.1) + lint_roller (~> 1.1) + rubocop (>= 1.75.0, < 2.0) + rubocop-ast (>= 1.38.0, < 2.0) + rubocop-performance (1.26.1) + lint_roller (~> 1.1) + rubocop (>= 1.75.0, < 2.0) + rubocop-ast (>= 1.47.1, < 2.0) + rubocop-rails (2.34.3) activesupport (>= 4.2.0) + lint_roller (~> 1.1) rack (>= 1.1) - rubocop (>= 1.33.0, < 2.0) - ruby-progressbar (1.11.0) - safe_yaml (1.0.5) + rubocop (>= 1.75.0, < 2.0) + rubocop-ast (>= 1.44.0, < 2.0) + ruby-progressbar (1.13.0) + securerandom (0.4.1) + stringio (3.2.0) + tsort (0.2.0) tzinfo (2.0.6) concurrent-ruby (~> 1.0) - unicode-display_width (2.4.2) - webmock (3.8.0) - addressable (>= 2.3.6) + unicode-display_width (3.2.0) + unicode-emoji (~> 4.1) + unicode-emoji (4.2.0) + uri (1.1.1) + webmock (3.26.1) + addressable (>= 2.8.0) crack (>= 0.3.2) hashdiff (>= 0.4.0, < 2.0.0) diff --git a/lib/rspamd-ruby.rb b/lib/rspamd-ruby.rb index 4617b9b..7787779 100644 --- a/lib/rspamd-ruby.rb +++ b/lib/rspamd-ruby.rb @@ -1,2 +1,37 @@ +require "active_support/core_ext/hash/keys" require "rspamd/client" +require "rspamd/client_stub" require "rspamd/errors" +require "rspamd/railtie" if defined?(::Rails::Railtie) + +module Rspamd + class << self + def setup(config) + @config = config.deep_symbolize_keys + @clients = {} + end + + def client_for(name) + clients[name] ||= enabled? ? build_client(name) : ClientStub.new + end + + def reset! + @config = nil + @clients = {} + end + + private + def clients + @clients ||= {} + end + + def enabled? + @config&.dig(:enabled) + end + + def build_client(name) + settings = @config.fetch(name) { raise ArgumentError, "No rspamd configuration for #{name.inspect}" } + Client.new(**settings.slice(:host, :port, :password)) + end + end +end diff --git a/lib/rspamd/client_stub.rb b/lib/rspamd/client_stub.rb new file mode 100644 index 0000000..2eaa72c --- /dev/null +++ b/lib/rspamd/client_stub.rb @@ -0,0 +1,39 @@ +require "rspamd/check/result" + +module Rspamd + class ClientStub + HAM_RESULT = Check::Result.new( + "score" => 0.0, + "required_score" => 15.0, + "action" => "no action", + "is_skipped" => false, + "symbols" => {}, + "urls" => [], + "emails" => [] + ).freeze + + def ping + true + end + + def check(message, headers: {}) + HAM_RESULT + end + + def spam!(message) + true + end + + def ham!(message) + true + end + + def add_fuzzy(message, flag: 1, weight: 1) + true + end + + def delete_fuzzy(message, flag: 1) + true + end + end +end diff --git a/lib/rspamd/railtie.rb b/lib/rspamd/railtie.rb new file mode 100644 index 0000000..f5b711e --- /dev/null +++ b/lib/rspamd/railtie.rb @@ -0,0 +1,11 @@ +module Rspamd + class Railtie < ::Rails::Railtie + initializer "rspamd.setup" do + config_path = ::Rails.root.join("config/rspamd.yml") + + if config_path.exist? + Rspamd.setup(::Rails.application.config_for(:rspamd)) + end + end + end +end diff --git a/rspamd-ruby.gemspec b/rspamd-ruby.gemspec index e7074f3..7512866 100644 --- a/rspamd-ruby.gemspec +++ b/rspamd-ruby.gemspec @@ -8,7 +8,9 @@ Gem::Specification.new do |s| s.summary = "Client for Rspamd's HTTP API" s.homepage = "https://github.com/basecamp/rspamd-ruby" - s.required_ruby_version = ">= 2.7.8" + s.required_ruby_version = ">= 3.2" + + s.add_dependency "activesupport", ">= 6.0" s.add_development_dependency "rake", "~> 13.0" s.add_development_dependency "minitest", "> 5.11" diff --git a/test/client_stub_test.rb b/test/client_stub_test.rb new file mode 100644 index 0000000..e5dc8ef --- /dev/null +++ b/test/client_stub_test.rb @@ -0,0 +1,46 @@ +require "test_helper" + +class Rspamd::ClientStubTest < Minitest::Test + def setup + @stub = Rspamd::ClientStub.new + end + + def test_ping_returns_true + assert @stub.ping + end + + def test_check_returns_ham_result + result = @stub.check("message body") + assert result.ham? + assert_not result.spam? + assert_equal 0.0, result.score + assert_equal 15.0, result.required_score + assert_equal "no action", result.action + end + + def test_check_accepts_headers + result = @stub.check("message body", headers: { "Settings-Id" => "outbound" }) + assert result.ham? + end + + def test_spam_returns_true + assert @stub.spam!("message body") + end + + def test_ham_returns_true + assert @stub.ham!("message body") + end + + def test_add_fuzzy_returns_true + assert @stub.add_fuzzy("message body") + end + + def test_delete_fuzzy_returns_true + assert @stub.delete_fuzzy("message body") + end + + private + def assert_not(value) + assert !value + end +end diff --git a/test/setup_test.rb b/test/setup_test.rb new file mode 100644 index 0000000..a47ec44 --- /dev/null +++ b/test/setup_test.rb @@ -0,0 +1,64 @@ +require "test_helper" + +class Rspamd::SetupTest < Minitest::Test + def teardown + Rspamd.reset! + end + + def test_returns_stub_when_disabled + Rspamd.setup("enabled" => false, "outbound" => { "host" => "localhost", "port" => 11334 }) + + client = Rspamd.client_for(:outbound) + assert_instance_of Rspamd::ClientStub, client + end + + def test_returns_client_when_enabled + Rspamd.setup("enabled" => true, "outbound" => { "host" => "rspamd.example.com", "port" => 11334 }) + + client = Rspamd.client_for(:outbound) + assert_instance_of Rspamd::Client, client + end + + def test_caches_client_instances + Rspamd.setup("enabled" => true, "outbound" => { "host" => "localhost", "port" => 11334 }) + + client1 = Rspamd.client_for(:outbound) + client2 = Rspamd.client_for(:outbound) + assert_same client1, client2 + end + + def test_returns_stub_when_not_configured + client = Rspamd.client_for(:outbound) + assert_instance_of Rspamd::ClientStub, client + end + + def test_raises_for_unknown_client_name + Rspamd.setup("enabled" => true, "outbound" => { "host" => "localhost", "port" => 11334 }) + + assert_raises(ArgumentError) { Rspamd.client_for(:nonexistent) } + end + + def test_reset_clears_config_and_clients + Rspamd.setup("enabled" => true, "outbound" => { "host" => "localhost", "port" => 11334 }) + Rspamd.client_for(:outbound) + Rspamd.reset! + + # After reset, should return stub (no config = disabled) + client = Rspamd.client_for(:outbound) + assert_instance_of Rspamd::ClientStub, client + end + + def test_accepts_symbol_keys + Rspamd.setup(enabled: true, outbound: { host: "localhost", port: 11334 }) + + client = Rspamd.client_for(:outbound) + assert_instance_of Rspamd::Client, client + end + + def test_passes_password_to_client + Rspamd.setup("enabled" => true, "outbound" => { "host" => "localhost", "port" => 11334, "password" => "secret" }) + + client = Rspamd.client_for(:outbound) + assert_equal "secret", client.configuration.password + end +end