From c81a6bee5f9ba046465ed0e84865405b46f60e38 Mon Sep 17 00:00:00 2001 From: DualFroz Date: Sun, 13 Sep 2026 19:03:43 +0200 Subject: [PATCH] test: cover PolicyTestArticlePolicy, edit? and RateLimiterDslTest --- test/test_policy.rb | 74 ++++++++++++++++++++++++++ test/test_rate_limiter.rb | 64 ++++++++++++++++++++++ test/test_rate_limiter_memory_store.rb | 53 ++++++++++++++++++ 3 files changed, 191 insertions(+) create mode 100644 test/test_policy.rb create mode 100644 test/test_rate_limiter.rb create mode 100644 test/test_rate_limiter_memory_store.rb diff --git a/test/test_policy.rb b/test/test_policy.rb new file mode 100644 index 0000000..e97d495 --- /dev/null +++ b/test/test_policy.rb @@ -0,0 +1,74 @@ +require 'minitest/autorun' +require 'prism' + +PolicyTestUser = Struct.new(:id) +PolicyTestArticle = Struct.new(:id, :owner_id) + +class PolicyTestArticlePolicy < Prism::Policy + def edit? + user && record.owner_id == user.id + end + alias_method :destroy?, :edit? +end + +class PolicyTestArticlePolicy::Scope < Prism::Policy::Scope + def resolve + scope.select { |article| user && article.owner_id == user.id } + end +end + +class PolicyEmptyPolicy < Prism::Policy +end + +class PolicyTest < Minitest::Test + def setup + @owner = PolicyTestUser.new(1) + @stranger = PolicyTestUser.new(2) + @article = PolicyTestArticle.new(10, 1) + end + + def test_owner_may_edit_and_destroy + policy = PolicyTestArticlePolicy.new(@owner, @article) + assert policy.edit? + assert policy.destroy? + end + + def test_stranger_may_not_edit_or_destroy + policy = PolicyTestArticlePolicy.new(@stranger, @article) + refute policy.edit? + refute policy.destroy? + end + + def test_undeclared_query_default_denies + policy = PolicyEmptyPolicy.new(@owner, @article) + refute policy.publish? + refute policy.archive? + end + + def test_non_predicate_missing_method_still_raises + policy = PolicyEmptyPolicy.new(@owner, @article) + assert_raises(NoMethodError) { policy.some_method } + end + + def test_for_record_resolves_the_conventional_policy_class + assert_equal PolicyTestArticlePolicy, Prism::Policy.for_record(@article) + assert_equal PolicyTestArticlePolicy, Prism::Policy.for_record(PolicyTestArticle) + end + + def test_scope_resolves_to_only_the_users_own_records + articles = [@article, PolicyTestArticle.new(11, 2)] + resolved = PolicyTestArticlePolicy::Scope.new(@owner, articles).resolve + assert_equal [@article], resolved + end + + def test_scope_base_class_requires_resolve_to_be_implemented + assert_raises(NotImplementedError) { Prism::Policy::Scope.new(@owner, []).resolve } + end + + def test_not_authorized_error_carries_record_and_query + error = Prism::Policy::NotAuthorizedError.new(@article, 'destroy?') + assert_equal @article, error.record + assert_equal 'destroy?', error.query + assert_includes error.message, 'destroy?' + end +end diff --git a/test/test_rate_limiter.rb b/test/test_rate_limiter.rb new file mode 100644 index 0000000..963c722 --- /dev/null +++ b/test/test_rate_limiter.rb @@ -0,0 +1,64 @@ +require 'minitest/autorun' +require 'prism' + +class RateLimiterDslTest < Minitest::Test + def setup + Prism::RateLimiter.reset! + end + + def teardown + Prism::RateLimiter.reset! + end + + def test_throttle_registers_a_named_rule + Prism::RateLimiter.throttle('logins/ip', limit: 5, period: 60) { |req| req } + rule = Prism::RateLimiter.throttles.fetch('logins/ip') + assert_equal 5, rule.limit + assert_equal 60, rule.period + end + + def test_blocklist_safelist_and_track_registries_are_independent + Prism::RateLimiter.blocklist('bad-actors') { |_req| true } + Prism::RateLimiter.safelist('internal') { |_req| false } + Prism::RateLimiter.track('all-requests') { |_req| true } + + assert_equal %w[bad-actors], Prism::RateLimiter.blocklists.keys + assert_equal %w[internal], Prism::RateLimiter.safelists.keys + assert_equal %w[all-requests], Prism::RateLimiter.tracks.keys + end + + def test_reset_clears_every_registry_and_the_store + Prism::RateLimiter.throttle('t', limit: 1, period: 60) { |_req| 'k' } + Prism::RateLimiter.store.count('t:k', 60) + + Prism::RateLimiter.reset! + + assert_empty Prism::RateLimiter.throttles + assert_equal 1, Prism::RateLimiter.store.count('t:k', 60) + end + + def test_on_notify_receives_type_name_request_and_payload + seen = [] + Prism::RateLimiter.on_notify { |type, name, _request, payload| seen << [type, name, payload] } + Prism::RateLimiter.notify(:throttle, 'logins/ip', :fake_request, '1.2.3.4') + assert_equal [[:throttle, 'logins/ip', '1.2.3.4']], seen + end + + def test_fail2ban_filter_allows_offenses_below_the_threshold + 3.times.each do |i| + blocked = Prism::RateLimiter.fail2ban_filter('1.2.3.4', maxretry: 3, findtime: 60, bantime: 300) { true } + assert_equal(i == 2, blocked) + end + end + + def test_fail2ban_filter_ignores_non_offenses + 5.times do + refute Prism::RateLimiter.fail2ban_filter('1.2.3.4', maxretry: 3, findtime: 60, bantime: 300) { false } + end + end + + def test_fail2ban_filter_keeps_blocking_once_banned_even_without_a_new_offense + 3.times { Prism::RateLimiter.fail2ban_filter('1.2.3.4', maxretry: 3, findtime: 60, bantime: 300) { true } } + assert Prism::RateLimiter.fail2ban_filter('1.2.3.4', maxretry: 3, findtime: 60, bantime: 300) { false } + end +end diff --git a/test/test_rate_limiter_memory_store.rb b/test/test_rate_limiter_memory_store.rb new file mode 100644 index 0000000..981e046 --- /dev/null +++ b/test/test_rate_limiter_memory_store.rb @@ -0,0 +1,53 @@ +require 'minitest/autorun' +require 'prism' + +class RateLimiterMemoryStoreTest < Minitest::Test + def setup + @store = Prism::RateLimiter::MemoryStore.new + end + + def test_count_increments_within_the_same_window + assert_equal 1, @store.count('ip:1', 60) + assert_equal 2, @store.count('ip:1', 60) + assert_equal 3, @store.count('ip:1', 60) + end + + def test_count_is_independent_per_key + assert_equal 1, @store.count('ip:1', 60) + assert_equal 1, @store.count('ip:2', 60) + end + + def test_count_resets_after_the_window_elapses + assert_equal 1, @store.count('ip:1', 0.05) + assert_equal 2, @store.count('ip:1', 0.05) + sleep 0.08 + assert_equal 1, @store.count('ip:1', 0.05) + end + + def test_banned_is_false_when_never_banned + refute @store.banned?('ip:1') + end + + def test_ban_makes_banned_true_until_it_expires + @store.ban!('ip:1', 0.05) + assert @store.banned?('ip:1') + sleep 0.08 + refute @store.banned?('ip:1') + end + + def test_reset_clears_windows_and_bans + @store.count('ip:1', 60) + @store.ban!('ip:2', 60) + @store.reset! + assert_equal 1, @store.count('ip:1', 60) + refute @store.banned?('ip:2') + end + + def test_concurrent_increments_do_not_lose_updates + threads = Array.new(20) do + Thread.new { 100.times { @store.count('shared', 60) } } + end + threads.each(&:join) + assert_equal 2001, @store.count('shared', 60) + end +end