mirror of
https://github.com/discourse/discourse.git
synced 2026-08-09 21:45:25 +08:00
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
64 lines
2 KiB
Ruby
Vendored
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
|