Skip to content

Commit a7bc394

Browse files
authored
FEATURE: apply plugin modifier for answers controller rate limiting (#369)
1 parent ba3d427 commit a7bc394

File tree

2 files changed

+95
-0
lines changed

2 files changed

+95
-0
lines changed

app/controllers/discourse_solved/answer_controller.rb

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,13 @@ def unaccept
3535

3636
def limit_accepts
3737
return if current_user.staff?
38+
run_rate_limiter =
39+
DiscoursePluginRegistry.apply_modifier(
40+
:solved_answers_controller_run_rate_limiter,
41+
true,
42+
current_user,
43+
)
44+
return if !run_rate_limiter
3845
RateLimiter.new(nil, "accept-hr-#{current_user.id}", 20, 1.hour).performed!
3946
RateLimiter.new(nil, "accept-min-#{current_user.id}", 4, 30.seconds).performed!
4047
end
Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
# frozen_string_literal: true
2+
3+
require "rails_helper"
4+
5+
describe DiscourseSolved::AnswerController do
6+
fab!(:user)
7+
fab!(:staff_user) { Fabricate(:admin) }
8+
fab!(:category)
9+
fab!(:topic) { Fabricate(:topic, category: category) }
10+
fab!(:p) { Fabricate(:post, topic: topic) }
11+
fab!(:solution_post) { Fabricate(:post, topic: topic) }
12+
13+
before do
14+
SiteSetting.solved_enabled = true
15+
SiteSetting.allow_solved_on_all_topics = true
16+
category.custom_fields[DiscourseSolved::ENABLE_ACCEPTED_ANSWERS_CUSTOM_FIELD] = "true"
17+
category.save_custom_fields
18+
19+
# Give permission to accept solutions
20+
user.update!(trust_level: 1)
21+
22+
# Make user the topic creator so they can accept answers
23+
topic.update!(user_id: user.id)
24+
end
25+
26+
describe "#accept" do
27+
context "with default rate limiting" do
28+
it "applies rate limits to regular users" do
29+
sign_in(user)
30+
31+
# Should be rate limited
32+
RateLimiter.any_instance.expects(:performed!).raises(RateLimiter::LimitExceeded.new(60))
33+
post "/solution/accept.json", params: { id: solution_post.id }
34+
expect(response.status).to eq(429)
35+
end
36+
37+
it "does not apply rate limits to staff" do
38+
sign_in(staff_user)
39+
40+
post "/solution/accept.json", params: { id: solution_post.id }
41+
expect(response.status).to eq(200)
42+
end
43+
end
44+
45+
context "with plugin modifier" do
46+
it "allows plugins to bypass rate limiting" do
47+
sign_in(user)
48+
# Create a plugin instance and register a modifier
49+
plugin_instance = Plugin::Instance.new
50+
modifier_block = Proc.new { |_, _| false }
51+
plugin_instance.register_modifier(
52+
:solved_answers_controller_run_rate_limiter,
53+
&modifier_block
54+
)
55+
56+
post "/solution/accept.json", params: { id: solution_post.id }
57+
expect(response.status).to eq(200)
58+
post "/solution/accept.json", params: { id: solution_post.id }
59+
expect(response.status).to eq(200)
60+
61+
# Unregister the modifier using DiscoursePluginRegistry
62+
DiscoursePluginRegistry.unregister_modifier(
63+
plugin_instance,
64+
:solved_answers_controller_run_rate_limiter,
65+
&modifier_block
66+
)
67+
end
68+
end
69+
end
70+
describe "#unaccept" do
71+
before do
72+
# Setup an accepted solution
73+
sign_in(user)
74+
post "/solution/accept.json", params: { id: solution_post.id }
75+
expect(response.status).to eq(200)
76+
sign_out
77+
end
78+
79+
it "applies rate limits to regular users" do
80+
sign_in(user)
81+
82+
# Should be rate limited
83+
RateLimiter.any_instance.expects(:performed!).raises(RateLimiter::LimitExceeded.new(60))
84+
post "/solution/unaccept.json", params: { id: solution_post.id }
85+
expect(response.status).to eq(429)
86+
end
87+
end
88+
end

0 commit comments

Comments
 (0)