0
0
Fork 0
mirror of https://github.com/discourse/discourse.git synced 2026-08-09 21:45:25 +08:00
discourse/plugins/discourse-post-voting/spec/requests/topics_controller_spec.rb
Régis Hanol 0a2a6b4871
FIX: Redirect over-range topic page URLs instead of 404 (#40224)
Requesting `/t/slug/id?page=N` with a page number beyond the topic's
last valid page raised `Discourse::NotFound` and returned 404. This
produced a long-running stream of "increase in 404 errors" reports in
Google Search Console, because crawlers (and our own
`sitemap_recent.xml`) hold cached page URLs that become stale when a
topic shrinks — most often after deleted posts or topic splits.

The 404 also masked a latent bug for staff: the original check compared
the requested page against `topic.posts_count`, a cached column that
excludes whispers and ignores active filters (`username_filters`,
`replies_to_post_number`, etc.). A staff user navigating to `?page=N`
covering whisper posts could be 404'd even though the page contained
content they were entitled to see.

Replace the 404 with a 301 redirect to the last valid page (or the
unparameterized topic URL if the topic fits in one page), using
`@topic_view.filtered_posts.count` — the count of posts visible to the
current viewer — so the math respects whispers, deletions visible to
staff, and active filters. The COUNT query is gated behind `page > 1` so
the common case (no `?page=` param) pays nothing extra.

Negative pages still raise 404; only the over-range case becomes a
redirect.

https://meta.discourse.org/t/96337
2026-05-22 18:48:59 +02:00

64 lines
2 KiB
Ruby
Vendored

# frozen_string_literal: true
describe TopicsController do
fab!(:user)
fab!(:topic) { Fabricate(:topic, subtype: Topic::POST_VOTING_SUBTYPE) }
fab!(:post) { create_post(topic: topic) }
fab!(:comment) { Fabricate(:post_voting_comment, raw: "this is a comment!", post: post) }
fab!(:answer) { create_post(topic: topic) }
fab!(:answer_2) { create_post(topic: topic) }
fab!(:answer_3) { create_post(topic: topic) }
fab!(:vote) do
PostVoting::VoteManager.vote(answer_2, user, direction: PostVotingVote.directions[:up])
end
fab!(:vote_2) do
PostVoting::VoteManager.vote(answer, user, direction: PostVotingVote.directions[:down])
end
before { SiteSetting.post_voting_enabled = true }
describe "#show" do
it "orders posts by number of votes for a Post Voting topic" do
get "/t/#{topic.id}.json"
expect(response.status).to eq(200)
payload = response.parsed_body
expect(payload["post_stream"]["posts"].map { |p| p["id"] }).to eq(
[post.id, answer_2.id, answer_3.id, answer.id],
)
end
it "redirects an over-range page to the last valid page" do
get "/t/#{topic.id}.json?page=2"
expect(response).to redirect_to("/t/#{topic.slug}/#{topic.id}.json")
end
it "orders posts by date of creation when 'activity' filter is provided" do
get "/t/#{topic.id}.json?filter=#{TopicView::ACTIVITY_FILTER}"
expect(response.status).to eq(200)
payload = response.parsed_body
expect(payload["post_stream"]["posts"].map { |p| p["id"] }).to eq(
[post.id, answer.id, answer_2.id, answer_3.id],
)
end
it "includes post_voting comments in crawler view" do
get "/t/#{topic.slug}/#{topic.id}", env: { "HTTP_USER_AGENT" => "Googlebot" }
expect(response.status).to eq(200)
expect(response.body).to match(
%r{<span class="post-voting-comments__comment-cooked" itemprop="text"><p>this is a comment!</p></span>},
)
expect(response.body).to match(%r{<span class="post-voting-answer-count__value">3</span>})
end
end
end