Skip to content
This repository was archived by the owner on Jul 22, 2025. It is now read-only.
Merged
Show file tree
Hide file tree
Changes from 4 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
Expand Up @@ -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
Expand Down
96 changes: 96 additions & 0 deletions spec/requests/answer_controller_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
# 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 mock plugin instance with enabled? method
plugin_instance = Object.new
def plugin_instance.enabled?
true
end

# Store the block in a variable so we can reference it for unregistration
block = ->(_, _) { false }

# Register modifier with proper parameters - using mock plugin instance instead of self
DiscoursePluginRegistry.register_modifier(
plugin_instance,
:solved_answers_controller_run_rate_limiter,
&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 with the same plugin instance and block
DiscoursePluginRegistry.unregister_modifier(
plugin_instance, # Using the same mock plugin instance
:solved_answers_controller_run_rate_limiter,
&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