Skip to content

FEATURE: Provide generic modifier interface for rate limiting. #369

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions app/controllers/discourse_solved/answer_controller.rb
Original file line number Diff line number Diff line change
@@ -35,6 +35,13 @@ def unaccept

def limit_accepts
return if current_user.staff?
run_rate_limiter =
DiscoursePluginRegistry.apply_modifier(
:solved_answers_controller_run_rate_limiter,
true,
current_user,
)
return if !run_rate_limiter
RateLimiter.new(nil, "accept-hr-#{current_user.id}", 20, 1.hour).performed!
RateLimiter.new(nil, "accept-min-#{current_user.id}", 4, 30.seconds).performed!
end
88 changes: 88 additions & 0 deletions spec/requests/answer_controller_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
# frozen_string_literal: true

require "rails_helper"

describe DiscourseSolved::AnswerController do
fab!(:user)
fab!(:staff_user) { Fabricate(:admin) }
fab!(:category)
fab!(:topic) { Fabricate(:topic, category: category) }
fab!(:p) { Fabricate(:post, topic: topic) }
fab!(:solution_post) { Fabricate(:post, topic: topic) }

before do
SiteSetting.solved_enabled = true
SiteSetting.allow_solved_on_all_topics = true
category.custom_fields[DiscourseSolved::ENABLE_ACCEPTED_ANSWERS_CUSTOM_FIELD] = "true"
category.save_custom_fields

# Give permission to accept solutions
user.update!(trust_level: 1)

# Make user the topic creator so they can accept answers
topic.update!(user_id: user.id)
end

describe "#accept" do
context "with default rate limiting" do
it "applies rate limits to regular users" do
sign_in(user)

# Should be rate limited
RateLimiter.any_instance.expects(:performed!).raises(RateLimiter::LimitExceeded.new(60))
post "/solution/accept.json", params: { id: solution_post.id }
expect(response.status).to eq(429)
end

it "does not apply rate limits to staff" do
sign_in(staff_user)

post "/solution/accept.json", params: { id: solution_post.id }
expect(response.status).to eq(200)
end
end

context "with plugin modifier" do
it "allows plugins to bypass rate limiting" do
sign_in(user)
# Create a plugin instance and register a modifier
plugin_instance = Plugin::Instance.new
modifier_block = Proc.new { |_, _| false }
plugin_instance.register_modifier(
:solved_answers_controller_run_rate_limiter,
&modifier_block
)

post "/solution/accept.json", params: { id: solution_post.id }
expect(response.status).to eq(200)
post "/solution/accept.json", params: { id: solution_post.id }
expect(response.status).to eq(200)

# Unregister the modifier using DiscoursePluginRegistry
DiscoursePluginRegistry.unregister_modifier(
plugin_instance,
:solved_answers_controller_run_rate_limiter,
&modifier_block
)
end
end
end
describe "#unaccept" do
before do
# Setup an accepted solution
sign_in(user)
post "/solution/accept.json", params: { id: solution_post.id }
expect(response.status).to eq(200)
sign_out
end

it "applies rate limits to regular users" do
sign_in(user)

# Should be rate limited
RateLimiter.any_instance.expects(:performed!).raises(RateLimiter::LimitExceeded.new(60))
post "/solution/unaccept.json", params: { id: solution_post.id }
expect(response.status).to eq(429)
end
end
end