0
0
Fork 0
mirror of https://github.com/discourse/discourse.git synced 2026-08-11 02:59:07 +08:00
discourse/spec/requests/user_actions_controller_spec.rb
Mark VanLandingham ca893a8608
FIX: Bound user-actions index limit at 100 (#42361)
## Summary

The public `GET /user_actions.json` endpoint accepted an unbounded
client-supplied `limit` and forwarded it directly into the activity
query, allowing an anonymous request to materialize and serialize an
arbitrarily large number of actions. The controller now caps the
effective limit at 100 while preserving the default of 30 and smaller
caller-requested values, so oversized requests return a bounded page
without excessive resource use. Public-action visibility and
authorization filtering are unchanged.

## Source

- Patch Triage: https://patch.discourse.org/patch-triage/1588
2026-08-05 11:52:53 -05:00

396 lines
12 KiB
Ruby
Vendored

# frozen_string_literal: true
RSpec.describe UserActionsController do
describe "GET index" do
subject(:user_actions) { get "/user_actions.json", params: params }
context "when 'username' is not specified" do
let(:params) { {} }
it "fails" do
user_actions
expect(response).to have_http_status :bad_request
end
end
context "when requesting more actions than the maximum page size" do
fab!(:user)
let(:params) { { username: user.username, filter: UserAction::REPLY, limit: 200 } }
before do
topic = Fabricate(:topic, user: user)
101.times do
post = Fabricate(:post, topic: topic, user: user)
UserAction.create!(
action_type: UserAction::REPLY,
user: user,
acting_user: user,
target_topic: topic,
target_post: post,
)
end
user.user_stat.update!(post_count: 101)
end
it "returns no more than 100 actions" do
user_actions
expect(response).to have_http_status :ok
expect(response.parsed_body["user_actions"].length).to eq(100)
end
end
context "when 'username' is specified" do
let(:username) { post.user.username }
let(:params) { { username: username } }
let(:actions) { response.parsed_body["user_actions"] }
let(:post) { create_post }
before do
UserActionManager.enable
post.user.user_stat.update!(post_count: 1)
end
it "renders list correctly" do
user_actions
expect(response).to have_http_status :ok
expect(actions.first).to include "acting_name" => post.user.name, "post_number" => 1
expect(actions.first).not_to include "email"
end
it "does not disclose activity when public profiles are hidden" do
SiteSetting.hide_user_profiles_from_public = true
user_actions
expect(response).to have_http_status :not_found
expect(response.body).not_to include(post.raw)
end
it "returns categories when lazy load categories is enabled" do
SiteSetting.lazy_load_categories_groups = "#{Group::AUTO_GROUPS[:anonymous_users]}"
user_actions
expect(response.status).to eq(200)
category_ids = response.parsed_body["categories"].map { |category| category["id"] }
expect(category_ids).to contain_exactly(post.topic.category.id)
end
context "when 'acting_username' is provided" do
let(:user) { Fabricate(:user) }
before do
sign_in(post.user)
PostActionNotifier.enable
PostActionCreator.like(user, post)
params[:acting_username] = user.username
end
it "filters its results" do
user_actions
expect(response).to have_http_status :ok
expect(actions.first).to include "acting_username" => user.username
end
end
context "when user's profile is hidden" do
fab!(:post)
before { post.user.user_option.update_column(:hide_profile, true) }
context "when `allow_users_to_hide_profile` is disabled" do
before { SiteSetting.allow_users_to_hide_profile = false }
it "succeeds" do
user_actions
expect(response).to have_http_status :ok
end
end
context "when `allow_users_to_hide_profile` is enabled" do
it "returns a 404" do
user_actions
expect(response).to have_http_status :not_found
end
end
end
context "when checking other users' activity" do
fab!(:another_user, :user)
context "when filter is omitted" do
it "does not return private action types for another user" do
another_user.user_stat.update!(post_count: 1)
topic = Fabricate(:topic, user: another_user)
post = Fabricate(:post, topic: topic, user: another_user)
UserAction.create!(
action_type: UserAction::RESPONSE,
user_id: another_user.id,
acting_user_id: another_user.id,
target_topic_id: topic.id,
target_post_id: post.id,
)
get "/user_actions.json", params: { username: another_user.username }
expect(response.status).to eq(200)
action_types = response.parsed_body["user_actions"].map { |a| a["action_type"] }
expect(action_types).not_to include(*UserAction.private_types)
end
end
context "when user is anonymous" do
UserAction.private_types.each do |action_type|
action_name = UserAction.types.key(action_type)
it "cannot list other users' actions of type: #{action_name}" do
list_and_check(action_type, 404)
end
end
end
context "when user is logged in" do
fab!(:user)
before { sign_in(user) }
UserAction.private_types.each do |action_type|
action_name = UserAction.types.key(action_type)
it "cannot list other users' actions of type: #{action_name}" do
list_and_check(action_type, 404)
end
end
end
context "when user is a moderator" do
fab!(:moderator)
before { sign_in(moderator) }
UserAction.private_types.each do |action_type|
action_name = UserAction.types.key(action_type)
it "cannot list other users' actions of type: #{action_name}" do
list_and_check(action_type, 404)
end
end
end
context "when user is an admin" do
fab!(:admin)
before { sign_in(admin) }
UserAction.private_types.each do |action_type|
action_name = UserAction.types.key(action_type)
it "can list other users' actions of type: #{action_name}" do
list_and_check(action_type, 200)
end
end
end
def list_and_check(action_type, expected_response)
get "/user_actions.json", params: { filter: action_type, username: another_user.username }
expect(response.status).to eq(expected_response)
end
end
context "when bad data is provided" do
fab!(:user)
let(:params) { { filter: filter, username: username, offset: offset, limit: limit } }
let(:filter) { "1,2" }
let(:username) { user.username }
let(:offset) { "0" }
let(:limit) { "10" }
%i[filter username offset limit].each do |parameter|
context "when providing bad data for '#{parameter}'" do
let(parameter) { { bad: "data" } }
it "doesn't raise an error" do
user_actions
expect(response).not_to have_http_status :error
end
end
end
end
end
end
describe "#show" do
fab!(:acting_user) { Fabricate(:user, refresh_auto_groups: true) }
fab!(:target_post) do
UserActionManager.enable
post = create_post(user: acting_user)
acting_user.user_stat.update!(post_count: 1)
post
end
fab!(:user_action) do
UserAction.find_by(user_id: acting_user.id, action_type: UserAction.types[:new_topic])
end
it "returns the action for a visible user with a public action" do
get "/user_actions/#{user_action.id}.json"
parsed = response.parsed_body["user_action"]
expect(response).to have_http_status :ok
expect(parsed["action_type"]).to eq(user_action.action_type)
expect(parsed["title"]).to eq(target_post.topic.title)
expect(parsed["excerpt"]).to eq(
PrettyText.excerpt(target_post.cooked, 300, keep_emoji_images: true),
)
end
it "returns 404 when the fallback first post is hidden" do
UserActionManager.enable
hidden_text = "hidden fallback excerpt token"
hidden_post = create_post(user: acting_user, raw: hidden_text)
hidden_post.update_columns(hidden: true)
user_action =
UserAction.find_by!(
user_id: acting_user.id,
action_type: UserAction::NEW_TOPIC,
target_topic_id: hidden_post.topic_id,
)
get "/user_actions/#{user_action.id}.json"
aggregate_failures do
expect(response).to have_http_status :not_found
expect(response.body).not_to include(hidden_text)
end
end
it "returns the action when the hidden fallback first post belongs to the viewer" do
UserActionManager.enable
hidden_text = "own hidden fallback excerpt token"
hidden_post = create_post(user: acting_user, raw: hidden_text)
hidden_post.update_columns(hidden: true)
user_action =
UserAction.find_by!(
user_id: acting_user.id,
action_type: UserAction::NEW_TOPIC,
target_topic_id: hidden_post.topic_id,
)
sign_in(acting_user)
get "/user_actions/#{user_action.id}.json"
parsed = response.parsed_body["user_action"]
aggregate_failures do
expect(response).to have_http_status :ok
expect(parsed["excerpt"]).to include(hidden_text)
end
end
it "returns 404 when a hidden target post has no owner" do
hidden_text = "hidden ownerless excerpt token"
hidden_reply = create_post(user: Fabricate(:user), topic: target_post.topic, raw: hidden_text)
hidden_reply.update_columns(hidden: true, user_id: nil)
user_action =
UserAction.create!(
action_type: UserAction::REPLY,
user_id: acting_user.id,
acting_user_id: acting_user.id,
target_topic_id: hidden_reply.topic_id,
target_post_id: hidden_reply.id,
)
sign_in(acting_user)
get "/user_actions/#{user_action.id}.json"
aggregate_failures do
expect(response).to have_http_status :not_found
expect(response.body).not_to include(hidden_text)
end
end
it "returns the action when the hidden target post belongs to the viewer" do
hidden_text = "own hidden target excerpt token"
hidden_reply = create_post(user: acting_user, topic: target_post.topic, raw: hidden_text)
hidden_reply.update_columns(hidden: true)
user_action =
UserAction.create!(
action_type: UserAction::REPLY,
user_id: acting_user.id,
acting_user_id: acting_user.id,
target_topic_id: hidden_reply.topic_id,
target_post_id: hidden_reply.id,
)
sign_in(acting_user)
get "/user_actions/#{user_action.id}.json"
parsed = response.parsed_body["user_action"]
aggregate_failures do
expect(response).to have_http_status :ok
expect(parsed["excerpt"]).to include(hidden_text)
end
end
it "returns 404 for an unlisted topic viewed anonymously" do
UserActionManager.enable
unlisted_text = "unlisted user action excerpt token"
unlisted_post = create_post(user: acting_user, raw: unlisted_text)
unlisted_post.topic.update!(visible: false)
user_action =
UserAction.find_by!(
user_id: acting_user.id,
action_type: UserAction::NEW_TOPIC,
target_topic_id: unlisted_post.topic_id,
)
get "/user_actions/#{user_action.id}.json"
aggregate_failures do
expect(response).to have_http_status :not_found
expect(response.body).not_to include(unlisted_text)
end
end
it "returns 404 for a non-existent action" do
get "/user_actions/-1.json"
expect(response).to have_http_status :not_found
end
it "returns 404 when the acting user has a hidden profile" do
SiteSetting.allow_users_to_hide_profile = true
acting_user.user_option.update!(hide_profile: true)
get "/user_actions/#{user_action.id}.json"
expect(response).to have_http_status :not_found
end
it "returns 404 when hide_user_activity_tab is enabled" do
SiteSetting.hide_user_activity_tab = true
get "/user_actions/#{user_action.id}.json"
expect(response).to have_http_status :not_found
end
it "returns 404 for a private action type viewed by a non-admin non-owner" do
liker = Fabricate(:user)
other_user = Fabricate(:user)
UserActionManager.enable
PostActionNotifier.enable
PostActionCreator.like(liker, target_post)
was_liked_action =
UserAction.find_by(user_id: acting_user.id, action_type: UserAction.types[:was_liked])
sign_in(other_user)
get "/user_actions/#{was_liked_action.id}.json"
expect(response).to have_http_status :not_found
end
end
end