mirror of
https://github.com/discourse/discourse.git
synced 2026-08-06 13:08:40 +08:00
Previously, the duplicate topic title check compared new titles against every topic on the site regardless of visibility: users were blocked by titles in categories they couldn't see or unlisted topics they couldn't find, the bare "Title has already been used" error gave no way to locate the conflict (and doubled as an existence oracle for hidden titles), and the behavior was controlled by two entangled boolean settings. This change scopes the check to the destination category plus whatever the acting user can actually see, links the conflicting topic in the error — safe by construction, since being blocked now implies being able to see it: > This title has already been used by [another topic](). It also consolidates the two booleans into a single `duplicate_topic_titles` enum (`disallowed` / `allowed_across_categories` / `allowed`), with existing values migrated and the old names kept as hidden deprecated aliases that admin search still resolves. Reported in https://meta.discourse.org/t/title-has-already-been-used-in-a-secure-category/123047 Note for self-hosters: env-provided settings can't be migrated — `DISCOURSE_ALLOW_DUPLICATE_TOPIC_TITLES=true` configs need to switch to `DISCOURSE_DUPLICATE_TOPIC_TITLES=allowed`.
8133 lines
274 KiB
Ruby
Vendored
8133 lines
274 KiB
Ruby
Vendored
# coding: utf-8
|
||
# frozen_string_literal: true
|
||
|
||
RSpec.describe TopicsController do
|
||
fab!(:topic)
|
||
fab!(:dest_topic, :topic)
|
||
fab!(:invisible_topic) { Fabricate(:topic, visible: false) }
|
||
|
||
fab!(:pm, :private_message_topic)
|
||
|
||
fab!(:user) { Fabricate(:user, refresh_auto_groups: true) }
|
||
fab!(:user_2) { Fabricate(:user, refresh_auto_groups: true) }
|
||
fab!(:post_author1) { Fabricate(:user, refresh_auto_groups: true) }
|
||
fab!(:post_author2, :user)
|
||
fab!(:post_author3, :user)
|
||
fab!(:post_author4, :user)
|
||
fab!(:post_author5, :user)
|
||
fab!(:post_author6, :user)
|
||
fab!(:moderator)
|
||
fab!(:admin)
|
||
fab!(:trust_level_0)
|
||
fab!(:trust_level_1)
|
||
fab!(:trust_level_4)
|
||
|
||
fab!(:category)
|
||
fab!(:tracked_category, :category)
|
||
fab!(:shared_drafts_category, :category)
|
||
fab!(:staff_category) do
|
||
Fabricate(:category).tap do |staff_category|
|
||
staff_category.set_permissions(staff: :full)
|
||
staff_category.save!
|
||
end
|
||
end
|
||
|
||
fab!(:group_user) { Fabricate(:group_user, user:) }
|
||
fab!(:tag)
|
||
|
||
before { SiteSetting.personal_message_enabled_groups = Group::AUTO_GROUPS[:everyone] }
|
||
|
||
describe "topic_header plugin outlet" do
|
||
fab!(:another_topic) { Fabricate(:topic, title: "Another topic by me") }
|
||
|
||
before { global_setting(:load_plugins?, true) }
|
||
|
||
it "renders the connector templates from multiple plugins" do
|
||
get "/t/#{topic.slug}/#{topic.id}"
|
||
|
||
expect(response.status).to eq(200)
|
||
expect(response.body).to include("Fixture from my_plugin template 1: #{topic.title}")
|
||
expect(response.body).to include("Fixture from my_plugin template 2: #{topic.title}")
|
||
expect(response.body).to include("Fixture from my_plugin_2 template 1: #{topic.title}")
|
||
expect(response.body).to include("Fixture from my_plugin_2 template 2: #{topic.title}")
|
||
expect(response.body).not_to include("Fixture from my_plugin_3 template 1: #{topic.title}")
|
||
|
||
get "/t/#{another_topic.slug}/#{another_topic.id}"
|
||
|
||
expect(response.status).to eq(200)
|
||
expect(response.body).to include("Fixture from my_plugin template 1: #{another_topic.title}")
|
||
expect(response.body).to include("Fixture from my_plugin template 2: #{another_topic.title}")
|
||
|
||
expect(response.body).to include(
|
||
"Fixture from my_plugin_2 template 1: #{another_topic.title}",
|
||
)
|
||
|
||
expect(response.body).to include(
|
||
"Fixture from my_plugin_2 template 2: #{another_topic.title}",
|
||
)
|
||
|
||
expect(response.body).not_to include(
|
||
"Fixture from my_plugin_3 template 1: #{another_topic.title}",
|
||
)
|
||
end
|
||
end
|
||
|
||
describe "#wordpress" do
|
||
before { sign_in(moderator) }
|
||
|
||
fab!(:p1) { Fabricate(:post, user: moderator) }
|
||
fab!(:p2) { Fabricate(:post, topic: p1.topic, user: moderator) }
|
||
|
||
it "returns the JSON in the format our wordpress plugin needs" do
|
||
SiteSetting.external_system_avatars_url = ""
|
||
|
||
get "/t/#{p1.topic.id}/wordpress.json", params: { best: 3 }
|
||
|
||
expect(response.status).to eq(200)
|
||
json = response.parsed_body
|
||
|
||
# The JSON has the data the wordpress plugin needs
|
||
expect(json["id"]).to eq(p1.topic.id)
|
||
expect(json["posts_count"]).to eq(2)
|
||
expect(json["filtered_posts_count"]).to eq(2)
|
||
|
||
# Posts
|
||
expect(json["posts"].size).to eq(1)
|
||
post = json["posts"][0]
|
||
expect(post["id"]).to eq(p2.id)
|
||
expect(post["username"]).to eq(moderator.username)
|
||
expect(post["avatar_template"]).to eq(
|
||
"#{Discourse.base_url_no_prefix}#{moderator.avatar_template}",
|
||
)
|
||
expect(post["name"]).to eq(moderator.name)
|
||
expect(post["created_at"]).to be_present
|
||
expect(post["cooked"]).to eq(p2.cooked)
|
||
|
||
# Participants
|
||
expect(json["participants"].size).to eq(1)
|
||
participant = json["participants"][0]
|
||
expect(participant["id"]).to eq(moderator.id)
|
||
expect(participant["username"]).to eq(moderator.username)
|
||
expect(participant["avatar_template"]).to eq(
|
||
"#{Discourse.base_url_no_prefix}#{moderator.avatar_template}",
|
||
)
|
||
end
|
||
|
||
it "does not error out when using invalid parameters" do
|
||
get "/t/#{p1.topic.id}/wordpress.json", params: { topic_id: 1, best: { leet: "haxx0r" } }
|
||
|
||
expect(response.status).to eq(400)
|
||
end
|
||
end
|
||
|
||
describe "#move_posts" do
|
||
before do
|
||
SiteSetting.min_topic_title_length = 2
|
||
SiteSetting.tagging_enabled = true
|
||
end
|
||
|
||
it "needs you to be logged in" do
|
||
post "/t/111/move-posts.json", params: { title: "blah", post_ids: [1, 2, 3] }
|
||
expect(response.status).to eq(403)
|
||
end
|
||
|
||
describe "moving to a new topic" do
|
||
fab!(:p1) { Fabricate(:post, user: user, post_number: 1) }
|
||
let(:p2) { Fabricate(:post, user: user, post_number: 2, topic: p1.topic) }
|
||
let(:topic) { p1.topic }
|
||
|
||
it "raises an error without post_ids" do
|
||
sign_in(moderator)
|
||
post "/t/#{topic.id}/move-posts.json", params: { title: "blah" }
|
||
expect(response.status).to eq(400)
|
||
end
|
||
|
||
it "raises an error when the user doesn't have permission to move the posts" do
|
||
sign_in(user)
|
||
|
||
post "/t/#{topic.id}/move-posts.json",
|
||
params: {
|
||
title: "blah",
|
||
post_ids: [p1.post_number, p2.post_number],
|
||
}
|
||
|
||
expect(response).to be_forbidden
|
||
end
|
||
|
||
it "raises an error when the OP is not a regular post" do
|
||
sign_in(moderator)
|
||
p2 =
|
||
Fabricate(
|
||
:post,
|
||
user: post_author1,
|
||
topic: topic,
|
||
post_number: 2,
|
||
post_type: Post.types[:whisper],
|
||
)
|
||
p3 = Fabricate(:post, user: post_author2, topic: topic, post_number: 3)
|
||
|
||
post "/t/#{topic.id}/move-posts.json", params: { title: "blah", post_ids: [p2.id, p3.id] }
|
||
expect(response.status).to eq(422)
|
||
|
||
result = response.parsed_body
|
||
|
||
expect(result["errors"]).to be_present
|
||
end
|
||
|
||
context "with success" do
|
||
before { sign_in(admin) }
|
||
|
||
it "returns success" do
|
||
expect do
|
||
post "/t/#{topic.id}/move-posts.json",
|
||
params: {
|
||
title: "Logan is a good movie",
|
||
post_ids: [p2.id],
|
||
category_id: category.id,
|
||
tags: %w[foo bar],
|
||
}
|
||
end.to change { Topic.count }.by(1).and change { Tag.count }.by(2)
|
||
|
||
expect(response.status).to eq(200)
|
||
|
||
result = response.parsed_body
|
||
|
||
expect(result["success"]).to eq(true)
|
||
|
||
new_topic = Topic.last
|
||
expect(result["url"]).to eq(new_topic.relative_url)
|
||
expect(new_topic.excerpt).to eq(p2.excerpt_for_topic)
|
||
expect(Tag.all.pluck(:name)).to include("foo", "bar")
|
||
end
|
||
|
||
it "moves posts to new topic with existing tags" do
|
||
tag1 = Fabricate(:tag, name: "existing-tag")
|
||
tag2 = Fabricate(:tag, name: "another-tag")
|
||
|
||
post "/t/#{topic.id}/move-posts.json",
|
||
params: {
|
||
title: "Topic with tags",
|
||
post_ids: [p2.id],
|
||
category_id: category.id,
|
||
tag_ids: [tag1.id, tag2.id],
|
||
}
|
||
|
||
expect(response.status).to eq(200)
|
||
result = response.parsed_body
|
||
expect(result["success"]).to eq(true)
|
||
|
||
new_topic = Topic.last
|
||
expect(new_topic.tags).to contain_exactly(tag1, tag2)
|
||
end
|
||
|
||
describe "with freeze_original param" do
|
||
it "duplicates post to new topic and keeps original post in place" do
|
||
expect do
|
||
post "/t/#{topic.id}/move-posts.json",
|
||
params: {
|
||
title: "Logan is a good movie",
|
||
post_ids: [p2.id],
|
||
freeze_original: true,
|
||
}
|
||
end.to change { Topic.count }.by(1)
|
||
expect(response.status).to eq(200)
|
||
expect(topic.post_ids).to include(p2.id)
|
||
end
|
||
end
|
||
|
||
describe "when topic has been deleted" do
|
||
it "should still be able to move posts" do
|
||
PostDestroyer.new(admin, topic.first_post, context: "Automated testing").destroy
|
||
|
||
expect(topic.reload.deleted_at).to_not be_nil
|
||
|
||
expect do
|
||
post "/t/#{topic.id}/move-posts.json",
|
||
params: {
|
||
title: "Logan is a good movie",
|
||
post_ids: [p2.id],
|
||
category_id: category.id,
|
||
}
|
||
end.to change { Topic.count }.by(1)
|
||
|
||
expect(response.status).to eq(200)
|
||
|
||
result = response.parsed_body
|
||
|
||
expect(result["success"]).to eq(true)
|
||
expect(result["url"]).to eq(Topic.last.relative_url)
|
||
end
|
||
end
|
||
end
|
||
|
||
context "with failure" do
|
||
it "returns JSON with a false success" do
|
||
sign_in(moderator)
|
||
post "/t/#{topic.id}/move-posts.json", params: { post_ids: [p2.id] }
|
||
expect(response.status).to eq(200)
|
||
result = response.parsed_body
|
||
expect(result["success"]).to eq(false)
|
||
expect(result["url"]).to be_blank
|
||
end
|
||
end
|
||
|
||
describe "moving replied posts" do
|
||
context "with success" do
|
||
it "moves the child posts too" do
|
||
sign_in(moderator)
|
||
p1 = Fabricate(:post, topic: topic, user: moderator)
|
||
p2 =
|
||
Fabricate(:post, topic: topic, user: moderator, reply_to_post_number: p1.post_number)
|
||
PostReply.create(post_id: p1.id, reply_post_id: p2.id)
|
||
|
||
post "/t/#{topic.id}/move-posts.json",
|
||
params: {
|
||
title: "new topic title",
|
||
post_ids: [p1.id],
|
||
reply_post_ids: [p1.id],
|
||
}
|
||
expect(response.status).to eq(200)
|
||
|
||
p1.reload
|
||
p2.reload
|
||
|
||
new_topic_id = response.parsed_body["url"].split("/").last.to_i
|
||
new_topic = Topic.find(new_topic_id)
|
||
expect(p1.topic.id).to eq(new_topic.id)
|
||
expect(p2.topic.id).to eq(new_topic.id)
|
||
expect(p2.reply_to_post_number).to eq(p1.post_number)
|
||
end
|
||
end
|
||
end
|
||
end
|
||
|
||
describe "moving to a new topic as a group moderator" do
|
||
fab!(:category)
|
||
fab!(:category_moderation_group) do
|
||
Fabricate(:category_moderation_group, category:, group: group_user.group)
|
||
end
|
||
fab!(:topic) { Fabricate(:topic, category: category) }
|
||
fab!(:p1) { Fabricate(:post, user: user, post_number: 1, topic: topic) }
|
||
fab!(:p2) { Fabricate(:post, user: user, post_number: 2, topic: topic) }
|
||
|
||
before do
|
||
sign_in(user)
|
||
SiteSetting.enable_category_group_moderation = true
|
||
end
|
||
|
||
it "moves the posts" do
|
||
expect do
|
||
post "/t/#{topic.id}/move-posts.json",
|
||
params: {
|
||
title: "Logan is a good movie",
|
||
post_ids: [p2.id],
|
||
category_id: category.id,
|
||
}
|
||
end.to change { Topic.count }.by(1)
|
||
|
||
expect(response.status).to eq(200)
|
||
result = response.parsed_body
|
||
expect(result["success"]).to eq(true)
|
||
expect(result["url"]).to eq(Topic.last.relative_url)
|
||
end
|
||
|
||
it "does not allow posts to be moved to a private category" do
|
||
post "/t/#{topic.id}/move-posts.json",
|
||
params: {
|
||
title: "Logan is a good movie",
|
||
post_ids: [p2.id],
|
||
category_id: staff_category.id,
|
||
}
|
||
|
||
expect(response).to be_forbidden
|
||
end
|
||
|
||
it "does not allow posts outside of the category to be moved" do
|
||
topic.update!(category: nil)
|
||
|
||
post "/t/#{topic.id}/move-posts.json",
|
||
params: {
|
||
title: "blah",
|
||
post_ids: [p1.post_number, p2.post_number],
|
||
}
|
||
|
||
expect(response).to be_forbidden
|
||
end
|
||
end
|
||
|
||
describe "moving to an existing topic" do
|
||
before { sign_in(moderator) }
|
||
|
||
fab!(:p1) { Fabricate(:post, user: moderator) }
|
||
fab!(:topic) { p1.topic }
|
||
fab!(:p2) { Fabricate(:post, user: moderator, topic: topic) }
|
||
|
||
context "with success" do
|
||
it "returns success" do
|
||
post "/t/#{topic.id}/move-posts.json",
|
||
params: {
|
||
post_ids: [p2.id],
|
||
destination_topic_id: dest_topic.id,
|
||
}
|
||
|
||
expect(response.status).to eq(200)
|
||
result = response.parsed_body
|
||
expect(result["success"]).to eq(true)
|
||
expect(result["url"]).to be_present
|
||
end
|
||
|
||
describe "with freeze_original param" do
|
||
it "duplicates post to topic and keeps original post in place" do
|
||
expect do
|
||
post "/t/#{topic.id}/move-posts.json",
|
||
params: {
|
||
post_ids: [p2.id],
|
||
destination_topic_id: dest_topic.id,
|
||
freeze_original: true,
|
||
}
|
||
end.to change { dest_topic.posts.count }.by(1)
|
||
expect(response.status).to eq(200)
|
||
expect(topic.post_ids).to include(p2.id)
|
||
expect(dest_topic.posts.find_by(raw: p2.raw)).to be_present
|
||
end
|
||
end
|
||
|
||
it "triggers an event on merge" do
|
||
called = false
|
||
|
||
assert = ->(original_topic, destination_topic) do
|
||
called = true
|
||
expect(original_topic).to eq(topic)
|
||
expect(destination_topic).to eq(dest_topic)
|
||
end
|
||
|
||
DiscourseEvent.on(:topic_merged, &assert)
|
||
|
||
post "/t/#{topic.id}/move-posts.json",
|
||
params: {
|
||
post_ids: [p2.id],
|
||
destination_topic_id: dest_topic.id,
|
||
}
|
||
|
||
expect(called).to eq(true)
|
||
expect(response.status).to eq(200)
|
||
ensure
|
||
DiscourseEvent.off(:topic_merged, &assert)
|
||
end
|
||
end
|
||
|
||
context "with failure" do
|
||
fab!(:p2) { Fabricate(:post, user: moderator) }
|
||
it "returns JSON with a false success" do
|
||
post "/t/#{topic.id}/move-posts.json", params: { post_ids: [p2.id] }
|
||
|
||
expect(response.status).to eq(200)
|
||
result = response.parsed_body
|
||
expect(result["success"]).to eq(false)
|
||
expect(result["url"]).to be_blank
|
||
end
|
||
|
||
it "returns plugin validation error" do
|
||
# stub here is to simulate validation added by plugin which would be triggered when post is moved
|
||
PostCreator.any_instance.stubs(:skip_validations?).returns(false)
|
||
|
||
p1.update_columns(raw: "i", cooked: "")
|
||
post "/t/#{topic.id}/move-posts.json",
|
||
params: {
|
||
post_ids: [p1.id],
|
||
destination_topic_id: dest_topic.id,
|
||
}
|
||
|
||
expect(response.status).to eq(422)
|
||
result = response.parsed_body
|
||
expect(result["errors"]).to eq(
|
||
[
|
||
"Body is too short (minimum is 5 characters) and Body seems unclear, is it a complete sentence?",
|
||
],
|
||
)
|
||
end
|
||
end
|
||
end
|
||
|
||
describe "moving to an existing topic in a read-only category as TL4" do
|
||
fab!(:readonly_category) do
|
||
Fabricate(:category).tap do |c|
|
||
c.set_permissions(everyone: :readonly, staff: :full)
|
||
c.save!
|
||
end
|
||
end
|
||
fab!(:source_topic, :topic)
|
||
fab!(:source_post) { Fabricate(:post, topic: source_topic, user: trust_level_4) }
|
||
fab!(:dest_topic_in_readonly) { Fabricate(:topic, category: readonly_category) }
|
||
|
||
before { sign_in(trust_level_4) }
|
||
|
||
it "is forbidden" do
|
||
post "/t/#{source_topic.id}/move-posts.json",
|
||
params: {
|
||
post_ids: [source_post.id],
|
||
destination_topic_id: dest_topic_in_readonly.id,
|
||
}
|
||
|
||
expect(response).to be_forbidden
|
||
end
|
||
end
|
||
|
||
describe "moving to an existing topic in a category with restricted write permissions as TL4" do
|
||
fab!(:group)
|
||
fab!(:restricted_write_category) do
|
||
Fabricate(:category).tap do |c|
|
||
c.set_permissions(group => :full, :everyone => :readonly)
|
||
c.save!
|
||
end
|
||
end
|
||
fab!(:source_topic, :topic)
|
||
fab!(:source_post) { Fabricate(:post, topic: source_topic, user: trust_level_4) }
|
||
fab!(:dest_topic_restricted) { Fabricate(:topic, category: restricted_write_category) }
|
||
|
||
before { sign_in(trust_level_4) }
|
||
|
||
it "is forbidden" do
|
||
post "/t/#{source_topic.id}/move-posts.json",
|
||
params: {
|
||
post_ids: [source_post.id],
|
||
destination_topic_id: dest_topic_restricted.id,
|
||
}
|
||
|
||
expect(response).to be_forbidden
|
||
end
|
||
end
|
||
|
||
describe "moving chronologically to an existing topic" do
|
||
before { sign_in(moderator) }
|
||
|
||
fab!(:p1) { Fabricate(:post, user: moderator, created_at: dest_topic.created_at - 1.hour) }
|
||
fab!(:topic) { p1.topic }
|
||
|
||
context "with success" do
|
||
it "returns success" do
|
||
post "/t/#{topic.id}/move-posts.json",
|
||
params: {
|
||
post_ids: [p1.id],
|
||
destination_topic_id: dest_topic.id,
|
||
chronological_order: "true",
|
||
}
|
||
|
||
expect(response.status).to eq(200)
|
||
result = response.parsed_body
|
||
expect(result["success"]).to eq(true)
|
||
expect(result["url"]).to be_present
|
||
end
|
||
end
|
||
end
|
||
|
||
describe "moving to an existing topic as a group moderator" do
|
||
fab!(:category)
|
||
fab!(:category_moderation_group) do
|
||
Fabricate(:category_moderation_group, category:, group: group_user.group)
|
||
end
|
||
fab!(:topic) { Fabricate(:topic, category: category) }
|
||
fab!(:p1) { Fabricate(:post, user: user, post_number: 1, topic: topic) }
|
||
fab!(:p2) { Fabricate(:post, user: user, post_number: 2, topic: topic) }
|
||
|
||
before do
|
||
sign_in(user)
|
||
SiteSetting.enable_category_group_moderation = true
|
||
end
|
||
|
||
it "moves the posts" do
|
||
post "/t/#{topic.id}/move-posts.json",
|
||
params: {
|
||
post_ids: [p2.id],
|
||
destination_topic_id: dest_topic.id,
|
||
}
|
||
|
||
expect(response.status).to eq(200)
|
||
result = response.parsed_body
|
||
expect(result["success"]).to eq(true)
|
||
expect(result["url"]).to be_present
|
||
end
|
||
|
||
it "does not allow posts to be moved to a private category" do
|
||
dest_topic.update!(category: staff_category)
|
||
|
||
post "/t/#{topic.id}/move-posts.json",
|
||
params: {
|
||
post_ids: [p2.id],
|
||
destination_topic_id: dest_topic.id,
|
||
}
|
||
|
||
expect(response).to be_forbidden
|
||
end
|
||
|
||
it "does not allow posts outside of the category to be moved" do
|
||
topic.update!(category: nil)
|
||
|
||
post "/t/#{topic.id}/move-posts.json",
|
||
params: {
|
||
post_ids: [p1.post_number, p2.post_number],
|
||
destination_topic_id: dest_topic.id,
|
||
}
|
||
|
||
expect(response).to be_forbidden
|
||
end
|
||
end
|
||
|
||
describe "moving chronologically to an existing topic as a group moderator" do
|
||
fab!(:category)
|
||
fab!(:category_moderation_group) do
|
||
Fabricate(:category_moderation_group, category:, group: group_user.group)
|
||
end
|
||
fab!(:topic) { Fabricate(:topic, category: category) }
|
||
fab!(:p1) do
|
||
Fabricate(:post, user: user, topic: topic, created_at: dest_topic.created_at - 1.hour)
|
||
end
|
||
|
||
before do
|
||
sign_in(user)
|
||
SiteSetting.enable_category_group_moderation = true
|
||
end
|
||
|
||
it "moves the posts" do
|
||
post "/t/#{topic.id}/move-posts.json",
|
||
params: {
|
||
post_ids: [p1.id],
|
||
destination_topic_id: dest_topic.id,
|
||
chronological_order: "true",
|
||
}
|
||
|
||
expect(response.status).to eq(200)
|
||
result = response.parsed_body
|
||
expect(result["success"]).to eq(true)
|
||
expect(result["url"]).to be_present
|
||
end
|
||
end
|
||
|
||
describe "moving to a new message" do
|
||
fab!(:message) { pm }
|
||
fab!(:p1) { Fabricate(:post, user: user, post_number: 1, topic: message) }
|
||
fab!(:p2) { Fabricate(:post, user: user, post_number: 2, topic: message) }
|
||
|
||
it "raises an error without post_ids" do
|
||
sign_in(moderator)
|
||
post "/t/#{message.id}/move-posts.json",
|
||
params: {
|
||
title: "blah",
|
||
archetype: "private_message",
|
||
}
|
||
expect(response.status).to eq(400)
|
||
end
|
||
|
||
it "raises an error when the user doesn't have permission to move the posts" do
|
||
sign_in(trust_level_4)
|
||
|
||
post "/t/#{message.id}/move-posts.json",
|
||
params: {
|
||
title: "blah",
|
||
post_ids: [p1.post_number, p2.post_number],
|
||
archetype: "private_message",
|
||
}
|
||
|
||
expect(response.status).to eq(403)
|
||
result = response.parsed_body
|
||
expect(result["errors"]).to be_present
|
||
end
|
||
|
||
context "with success" do
|
||
before { sign_in(admin) }
|
||
|
||
it "returns success" do
|
||
SiteSetting.pm_tags_allowed_for_groups = "1|2|3"
|
||
|
||
expect do
|
||
post "/t/#{message.id}/move-posts.json",
|
||
params: {
|
||
title: "Logan is a good movie",
|
||
post_ids: [p2.id],
|
||
archetype: "private_message",
|
||
tags: %w[foo bar],
|
||
}
|
||
end.to change { Topic.count }.by(1).and change { Tag.count }.by(2)
|
||
|
||
expect(response.status).to eq(200)
|
||
|
||
result = response.parsed_body
|
||
|
||
expect(result["success"]).to eq(true)
|
||
expect(result["url"]).to eq(Topic.last.relative_url)
|
||
expect(Tag.all.pluck(:name)).to include("foo", "bar")
|
||
end
|
||
|
||
describe "when message has been deleted" do
|
||
it "should still be able to move posts" do
|
||
PostDestroyer.new(admin, message.first_post).destroy
|
||
|
||
expect(message.reload.deleted_at).to_not be_nil
|
||
|
||
expect do
|
||
post "/t/#{message.id}/move-posts.json",
|
||
params: {
|
||
title: "Logan is a good movie",
|
||
post_ids: [p2.id],
|
||
archetype: "private_message",
|
||
}
|
||
end.to change { Topic.count }.by(1)
|
||
|
||
expect(response.status).to eq(200)
|
||
|
||
result = response.parsed_body
|
||
|
||
expect(result["success"]).to eq(true)
|
||
expect(result["url"]).to eq(Topic.last.relative_url)
|
||
end
|
||
end
|
||
end
|
||
|
||
context "with failure" do
|
||
it "returns JSON with a false success" do
|
||
sign_in(moderator)
|
||
post "/t/#{message.id}/move-posts.json",
|
||
params: {
|
||
post_ids: [p2.id],
|
||
archetype: "private_message",
|
||
}
|
||
expect(response.status).to eq(200)
|
||
result = response.parsed_body
|
||
expect(result["success"]).to eq(false)
|
||
expect(result["url"]).to be_blank
|
||
end
|
||
end
|
||
end
|
||
|
||
describe "moving to an existing message" do
|
||
before { sign_in(admin) }
|
||
|
||
fab!(:evil_trout)
|
||
fab!(:message) { pm }
|
||
fab!(:p2) { Fabricate(:post, user: evil_trout, post_number: 2, topic: message) }
|
||
|
||
fab!(:dest_message) do
|
||
Fabricate(
|
||
:private_message_topic,
|
||
user: trust_level_4,
|
||
topic_allowed_users: [Fabricate.build(:topic_allowed_user, user: evil_trout)],
|
||
)
|
||
end
|
||
|
||
context "with success" do
|
||
it "returns success" do
|
||
post "/t/#{message.id}/move-posts.json",
|
||
params: {
|
||
post_ids: [p2.id],
|
||
destination_topic_id: dest_message.id,
|
||
archetype: "private_message",
|
||
}
|
||
|
||
expect(response.status).to eq(200)
|
||
result = response.parsed_body
|
||
expect(result["success"]).to eq(true)
|
||
expect(result["url"]).to be_present
|
||
end
|
||
end
|
||
|
||
context "with failure" do
|
||
it "returns JSON with a false success" do
|
||
post "/t/#{message.id}/move-posts.json",
|
||
params: {
|
||
post_ids: [p2.id],
|
||
archetype: "private_message",
|
||
}
|
||
|
||
expect(response.status).to eq(200)
|
||
result = response.parsed_body
|
||
expect(result["success"]).to eq(false)
|
||
expect(result["url"]).to be_blank
|
||
end
|
||
end
|
||
end
|
||
|
||
describe "moving chronologically to an existing message" do
|
||
before { sign_in(admin) }
|
||
|
||
fab!(:evil_trout)
|
||
fab!(:message) { pm }
|
||
|
||
fab!(:dest_message) do
|
||
Fabricate(
|
||
:private_message_topic,
|
||
user: trust_level_4,
|
||
topic_allowed_users: [Fabricate.build(:topic_allowed_user, user: evil_trout)],
|
||
)
|
||
end
|
||
|
||
fab!(:p2) do
|
||
Fabricate(
|
||
:post,
|
||
user: evil_trout,
|
||
post_number: 2,
|
||
topic: message,
|
||
created_at: dest_message.created_at - 1.hour,
|
||
)
|
||
end
|
||
|
||
context "with success" do
|
||
it "returns success" do
|
||
post "/t/#{message.id}/move-posts.json",
|
||
params: {
|
||
post_ids: [p2.id],
|
||
destination_topic_id: dest_message.id,
|
||
archetype: "private_message",
|
||
chronological_order: "true",
|
||
}
|
||
|
||
expect(response.status).to eq(200)
|
||
result = response.parsed_body
|
||
expect(result["success"]).to eq(true)
|
||
expect(result["url"]).to be_present
|
||
end
|
||
end
|
||
end
|
||
end
|
||
|
||
describe "#merge_topic" do
|
||
it "needs you to be logged in" do
|
||
post "/t/111/merge-topic.json", params: { destination_topic_id: 345 }
|
||
expect(response.status).to eq(403)
|
||
end
|
||
|
||
describe "merging into another topic" do
|
||
fab!(:p1) { Fabricate(:post, user: user) }
|
||
fab!(:topic) { p1.topic }
|
||
|
||
it "raises an error without destination_topic_id" do
|
||
sign_in(moderator)
|
||
post "/t/#{topic.id}/merge-topic.json"
|
||
expect(response.status).to eq(400)
|
||
end
|
||
|
||
it "raises an error when the user doesn't have permission to merge" do
|
||
sign_in(user)
|
||
post "/t/111/merge-topic.json", params: { destination_topic_id: 345 }
|
||
expect(response).to be_forbidden
|
||
end
|
||
|
||
context "when moving all the posts to the destination topic" do
|
||
it "returns success" do
|
||
sign_in(moderator)
|
||
post "/t/#{topic.id}/merge-topic.json", params: { destination_topic_id: dest_topic.id }
|
||
|
||
expect(response.status).to eq(200)
|
||
result = response.parsed_body
|
||
expect(result["success"]).to eq(true)
|
||
expect(result["url"]).to be_present
|
||
end
|
||
end
|
||
end
|
||
|
||
describe "merging chronologically into another topic" do
|
||
fab!(:p1) { Fabricate(:post, user: user, created_at: dest_topic.created_at - 1.hour) }
|
||
fab!(:topic) { p1.topic }
|
||
|
||
context "when moving all the posts to the destination topic" do
|
||
it "returns success" do
|
||
sign_in(moderator)
|
||
post "/t/#{topic.id}/merge-topic.json",
|
||
params: {
|
||
destination_topic_id: dest_topic.id,
|
||
chronological_order: "true",
|
||
}
|
||
|
||
expect(response.status).to eq(200)
|
||
result = response.parsed_body
|
||
expect(result["success"]).to eq(true)
|
||
expect(result["url"]).to be_present
|
||
end
|
||
end
|
||
end
|
||
|
||
describe "merging into another topic as a group moderator" do
|
||
fab!(:category)
|
||
fab!(:category_moderation_group) do
|
||
Fabricate(:category_moderation_group, category:, group: group_user.group)
|
||
end
|
||
fab!(:topic) { Fabricate(:topic, category: category) }
|
||
fab!(:p1) { Fabricate(:post, user: post_author1, post_number: 1, topic: topic) }
|
||
fab!(:p2) { Fabricate(:post, user: post_author2, post_number: 2, topic: topic) }
|
||
|
||
before do
|
||
sign_in(user)
|
||
SiteSetting.enable_category_group_moderation = true
|
||
end
|
||
|
||
it "moves the posts" do
|
||
post "/t/#{topic.id}/merge-topic.json", params: { destination_topic_id: dest_topic.id }
|
||
|
||
expect(response.status).to eq(200)
|
||
result = response.parsed_body
|
||
expect(result["success"]).to eq(true)
|
||
expect(result["url"]).to be_present
|
||
end
|
||
|
||
it "does not allow posts to be moved to a private category" do
|
||
dest_topic.update!(category: staff_category)
|
||
|
||
post "/t/#{topic.id}/merge-topic.json", params: { destination_topic_id: dest_topic.id }
|
||
|
||
expect(response).to be_forbidden
|
||
end
|
||
|
||
it "does not allow posts outside of the category to be moved" do
|
||
topic.update!(category: nil)
|
||
|
||
post "/t/#{topic.id}/merge-topic.json", params: { destination_topic_id: dest_topic.id }
|
||
|
||
expect(response).to be_forbidden
|
||
end
|
||
end
|
||
|
||
describe "merging chronologically into another topic as a group moderator" do
|
||
fab!(:category)
|
||
fab!(:category_moderation_group) do
|
||
Fabricate(:category_moderation_group, category:, group: group_user.group)
|
||
end
|
||
fab!(:topic) { Fabricate(:topic, category: category) }
|
||
fab!(:p1) do
|
||
Fabricate(
|
||
:post,
|
||
user: post_author1,
|
||
post_number: 1,
|
||
topic: topic,
|
||
created_at: dest_topic.created_at - 1.hour,
|
||
)
|
||
end
|
||
fab!(:p2) do
|
||
Fabricate(
|
||
:post,
|
||
user: post_author2,
|
||
post_number: 2,
|
||
topic: topic,
|
||
created_at: dest_topic.created_at - 30.minutes,
|
||
)
|
||
end
|
||
|
||
before do
|
||
sign_in(user)
|
||
SiteSetting.enable_category_group_moderation = true
|
||
end
|
||
|
||
it "moves the posts" do
|
||
post "/t/#{topic.id}/merge-topic.json",
|
||
params: {
|
||
destination_topic_id: dest_topic.id,
|
||
chronological_order: "true",
|
||
}
|
||
|
||
expect(response.status).to eq(200)
|
||
result = response.parsed_body
|
||
expect(result["success"]).to eq(true)
|
||
expect(result["url"]).to be_present
|
||
end
|
||
end
|
||
|
||
describe "merging into another message" do
|
||
fab!(:message) { Fabricate(:private_message_topic, user: user) }
|
||
fab!(:p1) { Fabricate(:post, topic: message, user: trust_level_4) }
|
||
fab!(:p2) do
|
||
Fabricate(:post, topic: message, reply_to_post_number: p1.post_number, user: user)
|
||
end
|
||
|
||
fab!(:dest_message) do
|
||
Fabricate(
|
||
:private_message_topic,
|
||
user: trust_level_4,
|
||
topic_allowed_users: [Fabricate.build(:topic_allowed_user, user: moderator)],
|
||
)
|
||
end
|
||
|
||
it "raises an error without destination_topic_id" do
|
||
sign_in(moderator)
|
||
post "/t/#{message.id}/merge-topic.json", params: { archetype: "private_message" }
|
||
expect(response.status).to eq(400)
|
||
end
|
||
|
||
it "raises an error when the user doesn't have permission to merge" do
|
||
sign_in(trust_level_4)
|
||
post "/t/#{message.id}/merge-topic.json",
|
||
params: {
|
||
destination_topic_id: 345,
|
||
archetype: "private_message",
|
||
}
|
||
expect(response).to be_forbidden
|
||
end
|
||
|
||
context "when moving all the posts to the destination message" do
|
||
it "returns success" do
|
||
sign_in(moderator)
|
||
post "/t/#{message.id}/merge-topic.json",
|
||
params: {
|
||
destination_topic_id: dest_message.id,
|
||
archetype: "private_message",
|
||
}
|
||
|
||
expect(response.status).to eq(200)
|
||
result = response.parsed_body
|
||
expect(result["success"]).to eq(true)
|
||
expect(result["url"]).to be_present
|
||
end
|
||
end
|
||
end
|
||
|
||
describe "merging chronologically into another message" do
|
||
fab!(:message) { Fabricate(:private_message_topic, user: user) }
|
||
|
||
fab!(:dest_message) do
|
||
Fabricate(
|
||
:private_message_topic,
|
||
user: trust_level_4,
|
||
topic_allowed_users: [Fabricate.build(:topic_allowed_user, user: moderator)],
|
||
)
|
||
end
|
||
|
||
fab!(:p1) do
|
||
Fabricate(
|
||
:post,
|
||
topic: message,
|
||
user: trust_level_4,
|
||
created_at: dest_message.created_at - 1.hour,
|
||
)
|
||
end
|
||
fab!(:p2) do
|
||
Fabricate(
|
||
:post,
|
||
topic: message,
|
||
reply_to_post_number: p1.post_number,
|
||
user: user,
|
||
created_at: dest_message.created_at - 30.minutes,
|
||
)
|
||
end
|
||
|
||
context "when moving all the posts to the destination message" do
|
||
it "returns success" do
|
||
sign_in(moderator)
|
||
post "/t/#{message.id}/merge-topic.json",
|
||
params: {
|
||
destination_topic_id: dest_message.id,
|
||
archetype: "private_message",
|
||
chronological_order: "true",
|
||
}
|
||
|
||
expect(response.status).to eq(200)
|
||
result = response.parsed_body
|
||
expect(result["success"]).to eq(true)
|
||
expect(result["url"]).to be_present
|
||
end
|
||
end
|
||
end
|
||
|
||
describe "error handling" do
|
||
fab!(:p1) { Fabricate(:post, user: user) }
|
||
fab!(:topic) { p1.topic }
|
||
|
||
it "returns a JSON error when move_posts raises RecordInvalid" do
|
||
sign_in(moderator)
|
||
|
||
Topic
|
||
.any_instance
|
||
.stubs(:move_posts)
|
||
.raises(
|
||
ActiveRecord::RecordInvalid.new(
|
||
Post.new.tap { |p| p.errors.add(:base, "Something went wrong") },
|
||
),
|
||
)
|
||
|
||
post "/t/#{topic.id}/merge-topic.json", params: { destination_topic_id: dest_topic.id }
|
||
|
||
expect(response.status).to eq(422)
|
||
expect(response.parsed_body["errors"]).to be_present
|
||
end
|
||
end
|
||
end
|
||
|
||
describe "#change_post_owners" do
|
||
it "needs you to be logged in" do
|
||
post "/t/111/change-owner.json", params: { username: "user_a", post_ids: [1, 2, 3] }
|
||
expect(response).to be_forbidden
|
||
end
|
||
|
||
describe "forbidden to trust_level_4s" do
|
||
before { sign_in(trust_level_4) }
|
||
|
||
it "correctly denies" do
|
||
post "/t/111/change-owner.json",
|
||
params: {
|
||
topic_id: 111,
|
||
username: "user_a",
|
||
post_ids: [1, 2, 3],
|
||
}
|
||
expect(response).to be_forbidden
|
||
end
|
||
end
|
||
|
||
describe "changing ownership" do
|
||
fab!(:user_a, :user)
|
||
fab!(:p1) { Fabricate(:post, user: post_author1, topic: topic) }
|
||
fab!(:p2) { Fabricate(:post, user: post_author2, topic: topic) }
|
||
|
||
fab!(:allowed_group, :group)
|
||
fab!(:allowed_group_user) { Fabricate(:user, groups: [allowed_group]) }
|
||
|
||
describe "moderator signed in" do
|
||
let!(:editor) { sign_in(moderator) }
|
||
|
||
it "returns 200 when moderators_change_post_ownership is true" do
|
||
SiteSetting.moderators_change_post_ownership = true
|
||
|
||
post "/t/#{topic.id}/change-owner.json",
|
||
params: {
|
||
username: user_a.username_lower,
|
||
post_ids: [p1.id],
|
||
}
|
||
expect(response.status).to eq(200)
|
||
end
|
||
|
||
it "returns 403 when moderators_change_post_ownership is false" do
|
||
SiteSetting.moderators_change_post_ownership = false
|
||
|
||
post "/t/#{topic.id}/change-owner.json",
|
||
params: {
|
||
username: user_a.username_lower,
|
||
post_ids: [p1.id],
|
||
}
|
||
expect(response.status).to eq(403)
|
||
end
|
||
end
|
||
|
||
describe "admin signed in" do
|
||
let!(:editor) { sign_in(admin) }
|
||
|
||
it "raises an error with a parameter missing" do
|
||
[{ post_ids: [1, 2, 3] }, { username: "user_a" }].each do |params|
|
||
post "/t/111/change-owner.json", params: params
|
||
expect(response.status).to eq(400)
|
||
end
|
||
end
|
||
|
||
it "changes the topic and posts ownership" do
|
||
post "/t/#{topic.id}/change-owner.json",
|
||
params: {
|
||
username: user_a.username_lower,
|
||
post_ids: [p1.id],
|
||
}
|
||
topic.reload
|
||
p1.reload
|
||
expect(response.status).to eq(200)
|
||
expect(topic.user.username).to eq(user_a.username)
|
||
expect(p1.user.username).to eq(user_a.username)
|
||
end
|
||
|
||
it "changes multiple posts" do
|
||
post "/t/#{topic.id}/change-owner.json",
|
||
params: {
|
||
username: user_a.username_lower,
|
||
post_ids: [p1.id, p2.id],
|
||
}
|
||
|
||
expect(response.status).to eq(200)
|
||
|
||
p1.reload
|
||
p2.reload
|
||
|
||
expect(p1.user).to_not eq(nil)
|
||
expect(p1.reload.user).to eq(p2.reload.user)
|
||
end
|
||
|
||
it "works with deleted users" do
|
||
deleted_user = user
|
||
t2 = Fabricate(:topic, user: deleted_user)
|
||
p3 = Fabricate(:post, topic: t2, user: deleted_user)
|
||
|
||
UserDestroyer.new(editor).destroy(
|
||
deleted_user,
|
||
delete_posts: true,
|
||
context: "test",
|
||
delete_as_spammer: true,
|
||
)
|
||
|
||
post "/t/#{t2.id}/change-owner.json",
|
||
params: {
|
||
username: user_a.username_lower,
|
||
post_ids: [p3.id],
|
||
}
|
||
|
||
expect(response.status).to eq(200)
|
||
t2.reload
|
||
p3.reload
|
||
expect(t2.deleted_at).to be_nil
|
||
expect(p3.user).to eq(user_a)
|
||
end
|
||
|
||
it "removes likes by new owner" do
|
||
now = Time.zone.now
|
||
freeze_time(now - 1.day)
|
||
PostActionCreator.like(user_a, p1)
|
||
p1.reload
|
||
freeze_time(now)
|
||
post "/t/#{topic.id}/change-owner.json",
|
||
params: {
|
||
username: user_a.username_lower,
|
||
post_ids: [p1.id],
|
||
}
|
||
topic.reload
|
||
p1.reload
|
||
expect(response.status).to eq(200)
|
||
expect(topic.user.username).to eq(user_a.username)
|
||
expect(p1.user.username).to eq(user_a.username)
|
||
expect(p1.like_count).to eq(0)
|
||
end
|
||
end
|
||
|
||
describe "user in group signed in" do
|
||
fab!(:allowed_group, :group)
|
||
fab!(:allowed_group_user) { Fabricate(:user, groups: [allowed_group]) }
|
||
fab!(:topic_allowed_user_can_see) { Fabricate(:topic, category: category) }
|
||
fab!(:post_allowed_user_can_see) { Fabricate(:post, topic: topic_allowed_user_can_see) }
|
||
|
||
before { sign_in(allowed_group_user) }
|
||
|
||
it "returns 200 when group is allow listed" do
|
||
SiteSetting.change_post_ownership_allowed_groups = "#{allowed_group.id}"
|
||
|
||
post "/t/#{topic_allowed_user_can_see.id}/change-owner.json",
|
||
params: {
|
||
username: user_a.username_lower,
|
||
post_ids: [post_allowed_user_can_see.id],
|
||
}
|
||
expect(response.status).to eq(200)
|
||
end
|
||
|
||
it "returns 403 when group is not allow listed" do
|
||
SiteSetting.change_post_ownership_allowed_groups = ""
|
||
|
||
post "/t/#{topic_allowed_user_can_see.id}/change-owner.json",
|
||
params: {
|
||
username: user_a.username_lower,
|
||
post_ids: [post_allowed_user_can_see.id],
|
||
}
|
||
expect(response.status).to eq(403)
|
||
end
|
||
end
|
||
|
||
context "with API key" do
|
||
let(:api_key) { Fabricate(:api_key, user: admin, created_by: admin) }
|
||
|
||
it "allows changing ownership with change_owner scope" do
|
||
ApiKeyScope.create!(resource: "topics", action: "change_owner", api_key_id: api_key.id)
|
||
|
||
post "/t/#{topic.id}/change-owner.json",
|
||
params: {
|
||
username: user_a.username_lower,
|
||
post_ids: [p1.id],
|
||
},
|
||
headers: {
|
||
"HTTP_API_KEY" => api_key.key,
|
||
"HTTP_API_USERNAME" => api_key.user.username,
|
||
}
|
||
|
||
expect(response.status).to eq(200)
|
||
expect(p1.reload.user).to eq(user_a)
|
||
end
|
||
|
||
it "denies access without change_owner scope" do
|
||
ApiKeyScope.create!(resource: "topics", action: "read", api_key_id: api_key.id)
|
||
|
||
post "/t/#{topic.id}/change-owner.json",
|
||
params: {
|
||
username: user_a.username_lower,
|
||
post_ids: [p1.id],
|
||
},
|
||
headers: {
|
||
"HTTP_API_KEY" => api_key.key,
|
||
"HTTP_API_USERNAME" => api_key.user.username,
|
||
}
|
||
|
||
expect(response.status).to eq(403)
|
||
end
|
||
end
|
||
|
||
describe "private messages" do
|
||
fab!(:private_category) do
|
||
Fabricate(
|
||
:private_category,
|
||
group: Fabricate(:group),
|
||
permission_type: CategoryGroup.permission_types[:full],
|
||
)
|
||
end
|
||
fab!(:private_topic) { Fabricate(:topic, category: private_category) }
|
||
fab!(:private_post) { Fabricate(:post, topic: private_topic) }
|
||
|
||
fab!(:pm_user, :user)
|
||
fab!(:pm_topic) { Fabricate(:private_message_topic, user: pm_user) }
|
||
fab!(:pm_post) { Fabricate(:post, topic: pm_topic, user: pm_user) }
|
||
|
||
describe "moderator signed in" do
|
||
before do
|
||
SiteSetting.moderators_change_post_ownership = true
|
||
sign_in(moderator)
|
||
end
|
||
|
||
it "returns 403 for topics in private categories the moderator cannot see" do
|
||
post "/t/#{private_topic.id}/change-owner.json",
|
||
params: {
|
||
username: user_a.username,
|
||
post_ids: [private_post.id],
|
||
}
|
||
expect(response.status).to eq(403)
|
||
end
|
||
|
||
it "returns 403 for private messages the moderator is not a participant of" do
|
||
post "/t/#{pm_topic.id}/change-owner.json",
|
||
params: {
|
||
username: user_a.username,
|
||
post_ids: [pm_post.id],
|
||
}
|
||
expect(response.status).to eq(403)
|
||
end
|
||
end
|
||
|
||
describe "admin signed in" do
|
||
before { sign_in(admin) }
|
||
|
||
it "can change ownership of posts in private messages" do
|
||
post "/t/#{pm_topic.id}/change-owner.json",
|
||
params: {
|
||
username: user_a.username,
|
||
post_ids: [pm_post.id],
|
||
}
|
||
expect(response.status).to eq(200)
|
||
expect(pm_post.reload.user).to eq(user_a)
|
||
end
|
||
|
||
it "can change ownership of posts in private categories" do
|
||
post "/t/#{private_topic.id}/change-owner.json",
|
||
params: {
|
||
username: user_a.username,
|
||
post_ids: [private_post.id],
|
||
}
|
||
expect(response.status).to eq(200)
|
||
expect(private_post.reload.user).to eq(user_a)
|
||
end
|
||
end
|
||
|
||
describe "user in allowed group signed in" do
|
||
fab!(:pm_topic_allowed_user_can_see) do
|
||
Fabricate(:private_message_topic, user: allowed_group_user)
|
||
end
|
||
fab!(:pm_post_allowed_user_can_see) do
|
||
Fabricate(:post, topic: pm_topic_allowed_user_can_see, user: allowed_group_user)
|
||
end
|
||
|
||
before do
|
||
SiteSetting.change_post_ownership_allowed_groups = "#{allowed_group.id}"
|
||
sign_in(allowed_group_user)
|
||
end
|
||
|
||
it "returns 200 for visible PM" do
|
||
post "/t/#{pm_topic_allowed_user_can_see.id}/change-owner.json",
|
||
params: {
|
||
username: user_a.username_lower,
|
||
post_ids: [pm_post_allowed_user_can_see.id],
|
||
}
|
||
expect(response.status).to eq(200)
|
||
end
|
||
|
||
it "returns 403 for not visible PM" do
|
||
post "/t/#{pm_topic.id}/change-owner.json",
|
||
params: {
|
||
username: user_a.username,
|
||
post_ids: [pm_post.id],
|
||
}
|
||
expect(response.status).to eq(403)
|
||
end
|
||
|
||
it "returns 403 for post in private category" do
|
||
post "/t/#{private_topic.id}/change-owner.json",
|
||
params: {
|
||
username: user_a.username,
|
||
post_ids: [private_post.id],
|
||
}
|
||
expect(response.status).to eq(403)
|
||
end
|
||
end
|
||
end
|
||
end
|
||
end
|
||
|
||
describe "#change_timestamps" do
|
||
let!(:params) { { timestamp: Time.zone.now } }
|
||
|
||
it "needs you to be logged in" do
|
||
put "/t/1/change-timestamp.json", params: params
|
||
expect(response.status).to eq(403)
|
||
end
|
||
|
||
describe "forbidden to trust_level_4" do
|
||
before { sign_in(trust_level_4) }
|
||
|
||
it "correctly denies" do
|
||
put "/t/1/change-timestamp.json", params: params
|
||
expect(response).to be_forbidden
|
||
end
|
||
end
|
||
|
||
describe "changing timestamps" do
|
||
before do
|
||
freeze_time
|
||
sign_in(moderator)
|
||
end
|
||
|
||
let!(:old_timestamp) { Time.zone.now }
|
||
let!(:new_timestamp) { old_timestamp - 1.day }
|
||
let!(:topic) { Fabricate(:topic, created_at: old_timestamp) }
|
||
let!(:p1) { Fabricate(:post, user: post_author1, topic: topic, created_at: old_timestamp) }
|
||
let!(:p2) do
|
||
Fabricate(:post, user: post_author2, topic: topic, created_at: old_timestamp + 1.day)
|
||
end
|
||
|
||
it "should update the timestamps of selected posts" do
|
||
# try to see if we fail with invalid first
|
||
put "/t/1/change-timestamp.json"
|
||
expect(response.status).to eq(400)
|
||
|
||
put "/t/#{topic.id}/change-timestamp.json", params: { timestamp: new_timestamp.to_f }
|
||
|
||
expect(response.status).to eq(200)
|
||
expect(topic.reload.created_at).to eq_time(new_timestamp)
|
||
expect(p1.reload.created_at).to eq_time(new_timestamp)
|
||
expect(p2.reload.created_at).to eq_time(old_timestamp)
|
||
end
|
||
|
||
it "should create a staff log entry" do
|
||
put "/t/#{topic.id}/change-timestamp.json", params: { timestamp: new_timestamp.to_f }
|
||
|
||
log = UserHistory.last
|
||
expect(log.acting_user_id).to eq(moderator.id)
|
||
expect(log.topic_id).to eq(topic.id)
|
||
expect(log.new_value).to eq(new_timestamp.utc.to_s)
|
||
expect(log.previous_value).to eq(old_timestamp.utc.to_s)
|
||
end
|
||
end
|
||
end
|
||
|
||
describe "#clear_pin" do
|
||
it "needs you to be logged in" do
|
||
put "/t/1/clear-pin.json"
|
||
expect(response.status).to eq(403)
|
||
end
|
||
|
||
context "when logged in" do
|
||
before { sign_in(user) }
|
||
|
||
it "fails when the user can't see the topic" do
|
||
put "/t/#{pm.id}/clear-pin.json"
|
||
expect(response).to be_forbidden
|
||
end
|
||
|
||
describe "when the user can see the topic" do
|
||
it "succeeds" do
|
||
expect do put "/t/#{topic.id}/clear-pin.json" end.to change {
|
||
TopicUser.where(topic_id: topic.id, user_id: user.id).count
|
||
}.by(1)
|
||
expect(response.status).to eq(200)
|
||
end
|
||
end
|
||
end
|
||
end
|
||
|
||
describe "#status" do
|
||
it "needs you to be logged in" do
|
||
put "/t/1/status.json", params: { status: "visible", enabled: true }
|
||
expect(response.status).to eq(403)
|
||
end
|
||
|
||
describe "when logged in as a moderator" do
|
||
before { sign_in(moderator) }
|
||
|
||
it "raises an exception if you can't change it" do
|
||
sign_in(user)
|
||
put "/t/#{topic.id}/status.json", params: { status: "visible", enabled: "true" }
|
||
expect(response).to be_forbidden
|
||
end
|
||
|
||
it "requires the status parameter" do
|
||
put "/t/#{topic.id}/status.json", params: { enabled: true }
|
||
expect(response.status).to eq(400)
|
||
end
|
||
|
||
it "requires the enabled parameter" do
|
||
put "/t/#{topic.id}/status.json", params: { status: "visible" }
|
||
expect(response.status).to eq(400)
|
||
end
|
||
|
||
it "raises an error with a status not in the allowlist" do
|
||
put "/t/#{topic.id}/status.json", params: { status: "title", enabled: "true" }
|
||
expect(response.status).to eq(400)
|
||
end
|
||
|
||
it "should update the status of the topic correctly" do
|
||
closed_user_topic = Fabricate(:topic, user: user, closed: true)
|
||
Fabricate(:topic_timer, topic: closed_user_topic, status_type: TopicTimer.types[:open])
|
||
|
||
put "/t/#{closed_user_topic.id}/status.json", params: { status: "closed", enabled: "false" }
|
||
|
||
expect(response.status).to eq(200)
|
||
expect(closed_user_topic.reload.closed).to eq(false)
|
||
expect(closed_user_topic.topic_timers).to eq([])
|
||
|
||
body = response.parsed_body
|
||
|
||
expect(body["topic_status_update"]).to eq(nil)
|
||
end
|
||
|
||
it "should update the status when `enabled` is a truthy value" do
|
||
closed_user_topic = Fabricate(:topic, user: user, closed: false)
|
||
|
||
put "/t/#{closed_user_topic.id}/status.json", params: { status: "closed", enabled: "t" }
|
||
|
||
expect(response.status).to eq(200)
|
||
expect(closed_user_topic.reload.closed).to eq(true)
|
||
|
||
put "/t/#{closed_user_topic.id}/status.json", params: { status: "closed", enabled: "0" }
|
||
|
||
expect(response.status).to eq(200)
|
||
expect(closed_user_topic.reload.closed).to eq(false)
|
||
|
||
put "/t/#{closed_user_topic.id}/status.json", params: { status: "closed", enabled: true }
|
||
|
||
expect(response.status).to eq(200)
|
||
expect(closed_user_topic.reload.closed).to eq(true)
|
||
end
|
||
end
|
||
|
||
describe "when logged in as a group member with reviewable status" do
|
||
fab!(:category)
|
||
fab!(:category_moderation_group) do
|
||
Fabricate(:category_moderation_group, category:, group: group_user.group)
|
||
end
|
||
fab!(:topic) { Fabricate(:topic, category: category) }
|
||
|
||
before do
|
||
sign_in(user)
|
||
SiteSetting.enable_category_group_moderation = true
|
||
end
|
||
|
||
it "should allow a group moderator to close a topic" do
|
||
put "/t/#{topic.id}/status.json", params: { status: "closed", enabled: "true" }
|
||
|
||
expect(response.status).to eq(200)
|
||
expect(topic.reload.closed).to eq(true)
|
||
expect(topic.posts.last.action_code).to eq("closed.enabled")
|
||
end
|
||
|
||
it "should allow a group moderator to open a closed topic" do
|
||
topic.update!(closed: true)
|
||
|
||
expect do
|
||
put "/t/#{topic.id}/status.json", params: { status: "closed", enabled: "false" }
|
||
end.to change { topic.reload.posts.count }.by(1)
|
||
|
||
expect(response.status).to eq(200)
|
||
expect(topic.reload.closed).to eq(false)
|
||
expect(topic.posts.last.action_code).to eq("closed.disabled")
|
||
end
|
||
|
||
it "should allow a group moderator to archive a topic" do
|
||
expect do
|
||
put "/t/#{topic.id}/status.json", params: { status: "archived", enabled: "true" }
|
||
end.to change { topic.reload.posts.count }.by(1)
|
||
|
||
expect(response.status).to eq(200)
|
||
expect(topic.reload.archived).to eq(true)
|
||
expect(topic.posts.last.action_code).to eq("archived.enabled")
|
||
end
|
||
|
||
it "should allow a group moderator to unarchive an archived topic" do
|
||
topic.update!(archived: true)
|
||
|
||
put "/t/#{topic.id}/status.json", params: { status: "archived", enabled: "false" }
|
||
|
||
expect(response.status).to eq(200)
|
||
expect(topic.reload.archived).to eq(false)
|
||
expect(topic.posts.last.action_code).to eq("archived.disabled")
|
||
end
|
||
|
||
it "should allow a group moderator to pin a topic" do
|
||
put "/t/#{topic.id}/status.json",
|
||
params: {
|
||
status: "pinned",
|
||
enabled: "true",
|
||
until: 2.weeks.from_now,
|
||
}
|
||
|
||
expect(response.status).to eq(200)
|
||
expect(topic.reload.pinned_at).to_not eq(nil)
|
||
end
|
||
|
||
it "should allow a group moderator to unpin a topic" do
|
||
put "/t/#{topic.id}/status.json", params: { status: "pinned", enabled: "false" }
|
||
|
||
expect(response.status).to eq(200)
|
||
expect(topic.reload.pinned_at).to eq(nil)
|
||
end
|
||
|
||
it "should allow a group moderator to unlist a topic" do
|
||
put "/t/#{topic.id}/status.json", params: { status: "visible", enabled: "false" }
|
||
|
||
expect(response.status).to eq(200)
|
||
expect(topic.reload.visible).to eq(false)
|
||
expect(topic.reload.visibility_reason_id).to eq(
|
||
Topic.visibility_reasons[:manually_unlisted],
|
||
)
|
||
expect(topic.posts.last.action_code).to eq("visible.disabled")
|
||
end
|
||
|
||
it "should allow a group moderator to list an unlisted topic" do
|
||
topic.update!(visible: false)
|
||
|
||
put "/t/#{topic.id}/status.json", params: { status: "visible", enabled: "true" }
|
||
|
||
expect(response.status).to eq(200)
|
||
expect(topic.reload.visible).to eq(true)
|
||
expect(topic.reload.visibility_reason_id).to eq(
|
||
Topic.visibility_reasons[:manually_relisted],
|
||
)
|
||
expect(topic.posts.last.action_code).to eq("visible.enabled")
|
||
end
|
||
end
|
||
|
||
describe "when logged in as TL4 user" do
|
||
before { sign_in(trust_level_4) }
|
||
|
||
it "allows TL4 to close visible topics" do
|
||
put "/t/#{topic.id}/status.json", params: { status: "closed", enabled: "true" }
|
||
expect(response.status).to eq(200)
|
||
expect(topic.reload.closed).to eq(true)
|
||
end
|
||
|
||
it "prevents TL4 from closing restricted category topics" do
|
||
staff_topic = Fabricate(:topic, category: staff_category)
|
||
put "/t/#{staff_topic.id}/status.json", params: { status: "closed", enabled: "true" }
|
||
expect(response.status).to eq(403)
|
||
expect(staff_topic.reload.closed).to eq(false)
|
||
end
|
||
|
||
it "prevents TL4 from closing private messages" do
|
||
pm = Fabricate(:private_message_topic)
|
||
put "/t/#{pm.id}/status.json", params: { status: "closed", enabled: "true" }
|
||
expect(response.status).to eq(403)
|
||
expect(pm.reload.closed).to eq(false)
|
||
end
|
||
|
||
it "prevents TL4 from archiving restricted topics" do
|
||
staff_topic = Fabricate(:topic, category: staff_category)
|
||
put "/t/#{staff_topic.id}/status.json", params: { status: "archived", enabled: "true" }
|
||
expect(response.status).to eq(403)
|
||
expect(staff_topic.reload.archived).to eq(false)
|
||
end
|
||
|
||
it "prevents TL4 from pinning restricted topics" do
|
||
staff_topic = Fabricate(:topic, category: staff_category)
|
||
put "/t/#{staff_topic.id}/status.json", params: { status: "pinned", enabled: "true" }
|
||
expect(response.status).to eq(403)
|
||
expect(staff_topic.reload.pinned_at).to eq(nil)
|
||
end
|
||
|
||
it "prevents TL4 from toggling visibility of restricted topics" do
|
||
staff_topic = Fabricate(:topic, category: staff_category)
|
||
put "/t/#{staff_topic.id}/status.json", params: { status: "visible", enabled: "false" }
|
||
expect(response.status).to eq(403)
|
||
expect(staff_topic.reload.visible).to eq(true)
|
||
end
|
||
end
|
||
|
||
context "with API key" do
|
||
let(:api_key) { Fabricate(:api_key, user: moderator, created_by: moderator) }
|
||
|
||
context "when key scope has restricted params" do
|
||
before do
|
||
ApiKeyScope.create(
|
||
resource: "topics",
|
||
action: "update",
|
||
api_key_id: api_key.id,
|
||
allowed_parameters: {
|
||
"category_id" => ["#{topic.category_id}"],
|
||
},
|
||
)
|
||
end
|
||
|
||
it "fails to update topic status in an unpermitted category" do
|
||
put "/t/#{topic.id}/status.json",
|
||
params: {
|
||
status: "closed",
|
||
enabled: "true",
|
||
category_id: tracked_category.id,
|
||
},
|
||
headers: {
|
||
"HTTP_API_KEY" => api_key.key,
|
||
"HTTP_API_USERNAME" => api_key.user.username,
|
||
}
|
||
|
||
expect(response.status).to eq(403)
|
||
expect(response.body).to include(I18n.t("invalid_access"))
|
||
expect(topic.reload.closed).to eq(false)
|
||
end
|
||
|
||
it "fails without a category_id" do
|
||
put "/t/#{topic.id}/status.json",
|
||
params: {
|
||
status: "closed",
|
||
enabled: "true",
|
||
},
|
||
headers: {
|
||
"HTTP_API_KEY" => api_key.key,
|
||
"HTTP_API_USERNAME" => api_key.user.username,
|
||
}
|
||
|
||
expect(response.status).to eq(403)
|
||
expect(response.body).to include(I18n.t("invalid_access"))
|
||
expect(topic.reload.closed).to eq(false)
|
||
end
|
||
|
||
it "updates topic status in a permitted category" do
|
||
put "/t/#{topic.id}/status.json",
|
||
params: {
|
||
status: "closed",
|
||
enabled: "true",
|
||
category_id: topic.category_id,
|
||
},
|
||
headers: {
|
||
"HTTP_API_KEY" => api_key.key,
|
||
"HTTP_API_USERNAME" => api_key.user.username,
|
||
}
|
||
|
||
expect(response.status).to eq(200)
|
||
expect(topic.reload.closed).to eq(true)
|
||
end
|
||
end
|
||
|
||
context "when key scope has no param restrictions" do
|
||
before do
|
||
ApiKeyScope.create(
|
||
resource: "topics",
|
||
action: "update",
|
||
api_key_id: api_key.id,
|
||
allowed_parameters: {
|
||
},
|
||
)
|
||
end
|
||
|
||
it "updates topic status" do
|
||
put "/t/#{topic.id}/status.json",
|
||
params: {
|
||
status: "closed",
|
||
enabled: "true",
|
||
},
|
||
headers: {
|
||
"HTTP_API_KEY" => api_key.key,
|
||
"HTTP_API_USERNAME" => api_key.user.username,
|
||
}
|
||
|
||
expect(response.status).to eq(200)
|
||
expect(topic.reload.closed).to eq(true)
|
||
end
|
||
end
|
||
end
|
||
end
|
||
|
||
describe "#destroy_timings" do
|
||
it "needs you to be logged in" do
|
||
delete "/t/1/timings.json"
|
||
expect(response.status).to eq(403)
|
||
end
|
||
|
||
def topic_user_post_timings_count(user, topic)
|
||
[TopicUser, PostTiming].map { |klass| klass.where(user: user, topic: topic).count }
|
||
end
|
||
|
||
context "for last post only" do
|
||
it "should allow you to retain topic timing but remove last post only" do
|
||
freeze_time
|
||
|
||
post1 = create_post
|
||
user = post1.user
|
||
|
||
topic = post1.topic
|
||
|
||
post2 = create_post(topic_id: topic.id)
|
||
|
||
PostTiming.create!(topic: topic, user: user, post_number: 2, msecs: 100)
|
||
|
||
user.user_stat.update!(first_unread_at: Time.now + 1.week)
|
||
|
||
topic_user = TopicUser.find_by(topic_id: topic.id, user_id: user.id)
|
||
|
||
topic_user.update!(last_read_post_number: 2)
|
||
|
||
# ensure we have 2 notifications
|
||
# fake notification on topic but it is read
|
||
first_notification =
|
||
Notification.create!(
|
||
user_id: user.id,
|
||
topic_id: topic.id,
|
||
data: "{}",
|
||
read: true,
|
||
notification_type: 1,
|
||
)
|
||
|
||
freeze_time 1.minute.from_now
|
||
PostAlerter.post_created(post2)
|
||
|
||
second_notification =
|
||
user.notifications.where(topic_id: topic.id).order(created_at: :desc).first
|
||
second_notification.update!(read: true)
|
||
|
||
sign_in(user)
|
||
|
||
delete "/t/#{topic.id}/timings.json?last=1"
|
||
|
||
expect(PostTiming.where(topic: topic, user: user, post_number: 2).exists?).to eq(false)
|
||
expect(PostTiming.where(topic: topic, user: user, post_number: 1).exists?).to eq(true)
|
||
|
||
expect(TopicUser.where(topic: topic, user: user, last_read_post_number: 1).exists?).to eq(
|
||
true,
|
||
)
|
||
|
||
user.user_stat.reload
|
||
expect(user.user_stat.first_unread_at).to eq_time(topic.updated_at)
|
||
|
||
first_notification.reload
|
||
second_notification.reload
|
||
expect(first_notification.read).to eq(true)
|
||
expect(second_notification.read).to eq(false)
|
||
|
||
PostDestroyer.new(admin, post2).destroy
|
||
|
||
delete "/t/#{topic.id}/timings.json?last=1"
|
||
|
||
expect(PostTiming.where(topic: topic, user: user, post_number: 1).exists?).to eq(false)
|
||
expect(TopicUser.where(topic: topic, user: user, last_read_post_number: nil).exists?).to eq(
|
||
true,
|
||
)
|
||
end
|
||
end
|
||
|
||
context "when logged in" do
|
||
fab!(:user_topic) { Fabricate(:topic, user: user) }
|
||
fab!(:user_post) { Fabricate(:post, user: user, topic: user_topic, post_number: 2) }
|
||
|
||
before do
|
||
sign_in(user)
|
||
TopicUser.create!(topic: user_topic, user: user)
|
||
PostTiming.create!(topic: user_topic, user: user, post_number: 2, msecs: 1000)
|
||
end
|
||
|
||
it "deletes the forum topic user and post timings records" do
|
||
expect do delete "/t/#{user_topic.id}/timings.json" end.to change {
|
||
topic_user_post_timings_count(user, user_topic)
|
||
}.from([1, 1]).to([0, 0])
|
||
end
|
||
end
|
||
|
||
context "for private messages" do
|
||
fab!(:pm_post, :private_message_post)
|
||
fab!(:pm_topic) { pm_post.topic }
|
||
fab!(:pm_user) { pm_topic.user }
|
||
|
||
before do
|
||
sign_in(pm_user)
|
||
TopicUser.create!(
|
||
topic: pm_topic,
|
||
user: pm_user,
|
||
last_read_post_number: 1,
|
||
notification_level: TopicUser.notification_levels[:watching],
|
||
)
|
||
PostTiming.create!(topic: pm_topic, user: pm_user, post_number: 1, msecs: 1000)
|
||
end
|
||
|
||
it "publishes a message to update the client-side tracking state" do
|
||
messages =
|
||
MessageBus.track_publish(PrivateMessageTopicTrackingState.user_channel(pm_user.id)) do
|
||
delete "/t/#{pm_topic.id}/timings.json"
|
||
end
|
||
|
||
expect(messages.size).to eq(1)
|
||
expect(messages.first.data["message_type"]).to eq("read")
|
||
expect(messages.first.data["topic_id"]).to eq(pm_topic.id)
|
||
end
|
||
end
|
||
end
|
||
|
||
describe "#mute/unmute" do
|
||
it "needs you to be logged in" do
|
||
put "/t/99/mute.json"
|
||
expect(response.status).to eq(403)
|
||
end
|
||
end
|
||
|
||
describe "#recover" do
|
||
it "won't allow us to recover a topic when we're not logged in" do
|
||
put "/t/1/recover.json"
|
||
expect(response.status).to eq(403)
|
||
end
|
||
|
||
describe "when logged in" do
|
||
let!(:topic) { Fabricate(:topic, user: user, deleted_at: Time.now, deleted_by: moderator) }
|
||
let!(:post) do
|
||
Fabricate(
|
||
:post,
|
||
user: user,
|
||
topic: topic,
|
||
post_number: 1,
|
||
deleted_at: Time.now,
|
||
deleted_by: moderator,
|
||
)
|
||
end
|
||
|
||
describe "without access" do
|
||
it "raises an exception when the user doesn't have permission to delete the topic" do
|
||
sign_in(user)
|
||
put "/t/#{topic.id}/recover.json"
|
||
expect(response).to be_forbidden
|
||
end
|
||
end
|
||
|
||
context "with permission" do
|
||
before { sign_in(moderator) }
|
||
|
||
it "succeeds" do
|
||
put "/t/#{topic.id}/recover.json"
|
||
topic.reload
|
||
post.reload
|
||
expect(response.status).to eq(200)
|
||
expect(topic.trashed?).to be_falsey
|
||
expect(post.trashed?).to be_falsey
|
||
end
|
||
end
|
||
|
||
context "when logged in as a category group moderator who cannot see the topic" do
|
||
fab!(:mod_group, :group)
|
||
fab!(:cat_mod_user, :user)
|
||
fab!(:private_category) { Fabricate(:private_category, group: Fabricate(:group)) }
|
||
fab!(:private_topic) do
|
||
Fabricate(:topic, category: private_category, deleted_at: Time.now, deleted_by: moderator)
|
||
end
|
||
fab!(:private_post) do
|
||
Fabricate(
|
||
:post,
|
||
topic: private_topic,
|
||
post_number: 1,
|
||
deleted_at: Time.now,
|
||
deleted_by: moderator,
|
||
)
|
||
end
|
||
|
||
before do
|
||
SiteSetting.enable_category_group_moderation = true
|
||
Fabricate(:category_moderation_group, category: private_category, group: mod_group)
|
||
mod_group.add(cat_mod_user)
|
||
sign_in(cat_mod_user)
|
||
end
|
||
|
||
it "prevents recovering a topic the user cannot see" do
|
||
put "/t/#{private_topic.id}/recover.json"
|
||
|
||
expect(response).to be_forbidden
|
||
expect(private_topic.reload.trashed?).to be_truthy
|
||
end
|
||
end
|
||
|
||
context "when logged in as a category group moderator who can see the topic" do
|
||
fab!(:mod_group, :group)
|
||
fab!(:cat_mod_user, :user)
|
||
fab!(:private_category) { Fabricate(:private_category, group: Fabricate(:group)) }
|
||
fab!(:private_topic) do
|
||
Fabricate(:topic, category: private_category, deleted_at: Time.now, deleted_by: moderator)
|
||
end
|
||
fab!(:private_post) do
|
||
Fabricate(
|
||
:post,
|
||
topic: private_topic,
|
||
post_number: 1,
|
||
deleted_at: Time.now,
|
||
deleted_by: moderator,
|
||
)
|
||
end
|
||
|
||
before do
|
||
SiteSetting.enable_category_group_moderation = true
|
||
private_category.set_permissions(mod_group => :full)
|
||
private_category.save!
|
||
Fabricate(:category_moderation_group, category: private_category, group: mod_group)
|
||
mod_group.add(cat_mod_user)
|
||
sign_in(cat_mod_user)
|
||
end
|
||
|
||
it "allows recovering a topic the user can see" do
|
||
put "/t/#{private_topic.id}/recover.json"
|
||
|
||
expect(response.status).to eq(200)
|
||
expect(private_topic.reload.trashed?).to be_falsey
|
||
end
|
||
end
|
||
end
|
||
end
|
||
|
||
describe "#delete" do
|
||
it "won't allow us to delete a topic when we're not logged in" do
|
||
delete "/t/1.json"
|
||
expect(response.status).to eq(403)
|
||
end
|
||
|
||
describe "when logged in" do
|
||
fab!(:topic) { Fabricate(:topic, user: user, created_at: 48.hours.ago) }
|
||
fab!(:post) { Fabricate(:post, topic: topic, user: user, post_number: 1) }
|
||
|
||
describe "without access" do
|
||
it "raises an exception when the user doesn't have permission to delete the topic" do
|
||
sign_in(user)
|
||
delete "/t/#{topic.id}.json"
|
||
expect(response.status).to eq(422)
|
||
end
|
||
end
|
||
|
||
describe "with permission" do
|
||
before { sign_in(moderator) }
|
||
|
||
it "succeeds" do
|
||
delete "/t/#{topic.id}.json"
|
||
expect(response.status).to eq(200)
|
||
topic.reload
|
||
expect(topic.trashed?).to be_truthy
|
||
end
|
||
end
|
||
|
||
context "when logged in as a category group moderator who cannot see the topic" do
|
||
fab!(:mod_group, :group)
|
||
fab!(:cat_mod_user, :user)
|
||
fab!(:private_category) { Fabricate(:private_category, group: Fabricate(:group)) }
|
||
fab!(:private_topic) { Fabricate(:topic, category: private_category) }
|
||
fab!(:private_post) { Fabricate(:post, topic: private_topic, user: user, post_number: 1) }
|
||
|
||
before do
|
||
SiteSetting.enable_category_group_moderation = true
|
||
Fabricate(:category_moderation_group, category: private_category, group: mod_group)
|
||
mod_group.add(cat_mod_user)
|
||
sign_in(cat_mod_user)
|
||
end
|
||
|
||
it "prevents deleting a topic the user cannot see" do
|
||
delete "/t/#{private_topic.id}.json"
|
||
|
||
expect(response.status).to eq(422)
|
||
expect(private_topic.reload.trashed?).to be_falsey
|
||
end
|
||
end
|
||
|
||
context "when logged in as a category group moderator who can see the topic" do
|
||
fab!(:mod_group, :group)
|
||
fab!(:cat_mod_user, :user)
|
||
fab!(:private_category) { Fabricate(:private_category, group: Fabricate(:group)) }
|
||
fab!(:private_topic) { Fabricate(:topic, category: private_category) }
|
||
fab!(:private_post) { Fabricate(:post, topic: private_topic, user: user, post_number: 1) }
|
||
|
||
before do
|
||
SiteSetting.enable_category_group_moderation = true
|
||
private_category.set_permissions(mod_group => :full)
|
||
private_category.save!
|
||
Fabricate(:category_moderation_group, category: private_category, group: mod_group)
|
||
mod_group.add(cat_mod_user)
|
||
sign_in(cat_mod_user)
|
||
end
|
||
|
||
it "allows deleting a topic the user can see" do
|
||
delete "/t/#{private_topic.id}.json"
|
||
|
||
expect(response.status).to eq(200)
|
||
expect(private_topic.reload.trashed?).to be_truthy
|
||
end
|
||
end
|
||
end
|
||
|
||
describe "force destroy" do
|
||
fab!(:post) { Fabricate(:post, topic: topic, post_number: 1) }
|
||
|
||
before do
|
||
SiteSetting.can_permanently_delete = true
|
||
|
||
sign_in(admin)
|
||
end
|
||
|
||
it "force destroys all deleted small actions in topic too" do
|
||
small_action_post = Fabricate(:small_action, topic: topic)
|
||
PostDestroyer.new(Discourse.system_user, post, context: "Automated testing").destroy
|
||
PostDestroyer.new(
|
||
Discourse.system_user,
|
||
small_action_post,
|
||
context: "Automated testing",
|
||
).destroy
|
||
|
||
delete "/t/#{topic.id}.json", params: { force_destroy: true }
|
||
|
||
expect(response.status).to eq(200)
|
||
|
||
expect(Topic.find_by(id: topic.id)).to eq(nil)
|
||
expect(Post.find_by(id: post.id)).to eq(nil)
|
||
expect(Post.find_by(id: small_action_post.id)).to eq(nil)
|
||
end
|
||
|
||
it "creates a log and clean up previously recorded sensitive information" do
|
||
small_action_post = Fabricate(:small_action, topic: topic)
|
||
PostDestroyer.new(Discourse.system_user, post, context: "Automated testing").destroy
|
||
PostDestroyer.new(
|
||
Discourse.system_user,
|
||
small_action_post,
|
||
context: "Automated testing",
|
||
).destroy
|
||
|
||
delete "/t/#{topic.id}.json", params: { force_destroy: true }
|
||
|
||
expect(response.status).to eq(200)
|
||
|
||
expect(UserHistory.last).to have_attributes(
|
||
action: UserHistory.actions[:delete_topic_permanently],
|
||
acting_user_id: admin.id,
|
||
)
|
||
|
||
expect(UserHistory.where(topic_id: topic.id, details: "(permanently deleted)").count).to eq(
|
||
2,
|
||
)
|
||
end
|
||
|
||
it "does not allow to destroy topic if not all posts were force destroyed" do
|
||
_other_post = Fabricate(:post, topic: topic, post_number: 2)
|
||
PostDestroyer.new(Discourse.system_user, post, context: "Automated testing").destroy
|
||
|
||
delete "/t/#{topic.id}.json", params: { force_destroy: true }
|
||
|
||
expect(response.status).to eq(403)
|
||
end
|
||
|
||
it "does not allow to destroy topic if not all small action posts were deleted" do
|
||
small_action_post = Fabricate(:small_action, topic: topic)
|
||
PostDestroyer.new(
|
||
Discourse.system_user,
|
||
small_action_post,
|
||
context: "Automated testing",
|
||
).destroy
|
||
|
||
delete "/t/#{topic.id}.json", params: { force_destroy: true }
|
||
|
||
expect(response.status).to eq(403)
|
||
end
|
||
end
|
||
end
|
||
|
||
describe "#id_for_slug" do
|
||
fab!(:topic) { Fabricate(:post, user: post_author1).topic }
|
||
|
||
it "returns JSON for the slug" do
|
||
get "/t/id_for/#{topic.slug}.json"
|
||
expect(response.status).to eq(200)
|
||
json = response.parsed_body
|
||
expect(json["topic_id"]).to eq(topic.id)
|
||
expect(json["url"]).to eq(topic.url)
|
||
expect(json["slug"]).to eq(topic.slug)
|
||
end
|
||
|
||
it "returns invalid access if the user can't see the topic" do
|
||
get "/t/id_for/#{pm.slug}.json"
|
||
expect(response).to be_forbidden
|
||
end
|
||
end
|
||
|
||
describe "#update" do
|
||
it "won't allow us to update a topic when we're not logged in" do
|
||
put "/t/1.json", params: { slug: "xyz" }
|
||
expect(response.status).to eq(403)
|
||
end
|
||
|
||
describe "when logged in" do
|
||
fab!(:topic) { Fabricate(:topic, user: user) }
|
||
|
||
before_all { Fabricate(:post, user: post_author1, topic: topic) }
|
||
|
||
before do
|
||
SiteSetting.editing_grace_period = 0
|
||
sign_in(user)
|
||
end
|
||
|
||
it "can not change category to a disallowed category" do
|
||
category.set_permissions(staff: :full)
|
||
category.save!
|
||
|
||
put "/t/#{topic.id}.json", params: { category_id: category.id }
|
||
|
||
expect(response.status).to eq(403)
|
||
expect(topic.reload.category_id).not_to eq(category.id)
|
||
end
|
||
|
||
it "can not move to a category that requires topic approval" do
|
||
category.require_topic_approval = true
|
||
category.save!
|
||
|
||
put "/t/#{topic.id}.json", params: { category_id: category.id }
|
||
|
||
expect(response.status).to eq(403)
|
||
expect(response.parsed_body["errors"].first).to eq(
|
||
I18n.t("category.errors.move_topic_to_category_disallowed"),
|
||
)
|
||
expect(topic.reload.category_id).not_to eq(category.id)
|
||
end
|
||
|
||
context "when updating shared drafts" do
|
||
fab!(:topic) { Fabricate(:topic, category: shared_drafts_category) }
|
||
fab!(:shared_draft) do
|
||
Fabricate(:shared_draft, topic: topic, category: Fabricate(:category))
|
||
end
|
||
|
||
it "changes destination category" do
|
||
put "/t/#{topic.id}.json", params: { category_id: category.id }
|
||
|
||
expect(response.status).to eq(403)
|
||
expect(topic.shared_draft.category_id).not_to eq(category.id)
|
||
end
|
||
end
|
||
|
||
it "does not allow a regular user to change archetype to banner" do
|
||
put "/t/#{topic.id}.json", params: { archetype: Archetype.banner }
|
||
|
||
topic.reload
|
||
expect(topic.archetype).to eq(Archetype.default)
|
||
end
|
||
|
||
it "does not allow a regular user to convert a private message to a public topic" do
|
||
private_message = Fabricate(:private_message_topic, user: user, recipient: user_2)
|
||
Fabricate(:post, topic: private_message, user: user)
|
||
victim_reply = Fabricate(:post, topic: private_message, user: user_2, raw: "private reply")
|
||
|
||
sign_in(post_author2)
|
||
get "/t/#{private_message.slug}/#{private_message.id}.json"
|
||
blocked_status = response.status
|
||
expect(blocked_status).to be_in([403, 404])
|
||
expect(response.body).not_to include(victim_reply.raw)
|
||
|
||
sign_in(user)
|
||
put "/t/#{private_message.slug}/#{private_message.id}.json",
|
||
params: {
|
||
archetype: Archetype.default,
|
||
category_id: category.id,
|
||
}
|
||
update_status = response.status
|
||
update_body = response.parsed_body
|
||
|
||
sign_in(post_author2)
|
||
get "/t/#{private_message.slug}/#{private_message.id}.json"
|
||
|
||
aggregate_failures do
|
||
expect(update_status).to eq(422)
|
||
expect(update_body["errors"]).to include(
|
||
I18n.t("activerecord.errors.models.topic.attributes.base.unable_to_update"),
|
||
)
|
||
expect(private_message.reload).to be_private_message
|
||
expect(private_message.category_id).to be_nil
|
||
expect(response.status).to eq(blocked_status)
|
||
expect(response.body).not_to include(victim_reply.raw)
|
||
end
|
||
end
|
||
|
||
describe "without permission" do
|
||
it "raises an exception when the user doesn't have permission to update the topic" do
|
||
topic.update!(archived: true)
|
||
put "/t/#{topic.slug}/#{topic.id}.json"
|
||
|
||
expect(response.status).to eq(403)
|
||
end
|
||
end
|
||
|
||
context "with permission" do
|
||
fab!(:post_hook, :post_web_hook)
|
||
fab!(:topic_hook, :topic_web_hook)
|
||
|
||
it "succeeds" do
|
||
put "/t/#{topic.slug}/#{topic.id}.json"
|
||
|
||
expect(response.status).to eq(200)
|
||
expect(response.parsed_body["basic_topic"]).to be_present
|
||
end
|
||
|
||
it "prevents conflicts when title was changed" do
|
||
put "/t/#{topic.slug}/#{topic.id}.json",
|
||
params: {
|
||
title: "brand new title",
|
||
original_title: "another title",
|
||
}
|
||
|
||
expect(response.status).to eq(409)
|
||
expect(response.parsed_body["errors"].first).to eq(I18n.t("edit_conflict"))
|
||
end
|
||
|
||
it "prevents conflicts when tags were changed" do
|
||
put "/t/#{topic.slug}/#{topic.id}.json",
|
||
params: {
|
||
tags: %w[tag1 tag2],
|
||
original_tags: %w[tag3 tag4],
|
||
}
|
||
|
||
expect(response.status).to eq(409)
|
||
expect(response.parsed_body["errors"].first).to eq(I18n.t("edit_conflict"))
|
||
end
|
||
|
||
it "throws an error if it could not be saved" do
|
||
PostRevisor.any_instance.stubs(:should_revise?).returns(false)
|
||
put "/t/#{topic.slug}/#{topic.id}.json", params: { title: "brand new title" }
|
||
|
||
expect(response.status).to eq(422)
|
||
expect(response.parsed_body["errors"].first).to eq(
|
||
I18n.t("activerecord.errors.models.topic.attributes.base.unable_to_update"),
|
||
)
|
||
end
|
||
|
||
it "can update a topic to an uncategorized topic" do
|
||
topic.update!(category: category)
|
||
|
||
put "/t/#{topic.slug}/#{topic.id}.json", params: { category_id: "" }
|
||
|
||
expect(response.status).to eq(200)
|
||
expect(topic.reload.category_id).to eq(SiteSetting.uncategorized_category_id)
|
||
end
|
||
|
||
it "allows a change of title" do
|
||
put "/t/#{topic.slug}/#{topic.id}.json",
|
||
params: {
|
||
title: "This is a new title for the topic",
|
||
}
|
||
|
||
topic.reload
|
||
expect(topic.title).to eq("This is a new title for the topic")
|
||
|
||
# emits a topic_edited event but not a post_edited web hook event
|
||
expect(Jobs::EmitWebHookEvent.jobs.length).to eq(1)
|
||
job_args = Jobs::EmitWebHookEvent.jobs[0]["args"].first
|
||
|
||
expect(job_args["event_name"]).to eq("topic_edited")
|
||
payload = JSON.parse(job_args["payload"])
|
||
expect(payload["title"]).to eq("This is a new title for the topic")
|
||
end
|
||
|
||
it "allows update on short non-slug url" do
|
||
put "/t/#{topic.id}.json", params: { title: "This is a new title for the topic" }
|
||
|
||
topic.reload
|
||
expect(topic.title).to eq("This is a new title for the topic")
|
||
end
|
||
|
||
it "only allows update on digit ids" do
|
||
non_digit_id = "asdf"
|
||
original_title = topic.title
|
||
put "/t/#{non_digit_id}.json", params: { title: "This is a new title for the topic" }
|
||
|
||
topic.reload
|
||
expect(topic.title).to eq(original_title)
|
||
expect(response.status).to eq(404)
|
||
end
|
||
|
||
it "allows a change of then updating the OP" do
|
||
topic.update(user: user)
|
||
topic.first_post.update(user: user)
|
||
|
||
put "/t/#{topic.slug}/#{topic.id}.json",
|
||
params: {
|
||
title: "This is a new title for the topic",
|
||
}
|
||
|
||
topic.reload
|
||
expect(topic.title).to eq("This is a new title for the topic")
|
||
|
||
update_params = { post: { raw: "edited body", edit_reason: "typo" } }
|
||
put "/posts/#{topic.first_post.id}.json", params: update_params
|
||
|
||
# emits a topic_edited event and a post_edited web hook event
|
||
expect(Jobs::EmitWebHookEvent.jobs.length).to eq(2)
|
||
job_args = Jobs::EmitWebHookEvent.jobs[0]["args"].first
|
||
|
||
expect(job_args["event_name"]).to eq("topic_edited")
|
||
payload = JSON.parse(job_args["payload"])
|
||
expect(payload["title"]).to eq("This is a new title for the topic")
|
||
|
||
job_args = Jobs::EmitWebHookEvent.jobs[1]["args"].first
|
||
|
||
expect(job_args["event_name"]).to eq("post_edited")
|
||
payload = JSON.parse(job_args["payload"])
|
||
expect(payload["raw"]).to eq("edited body")
|
||
end
|
||
|
||
it "returns errors with invalid titles" do
|
||
put "/t/#{topic.slug}/#{topic.id}.json", params: { title: "asdf" }
|
||
|
||
expect(response.status).to eq(422)
|
||
expect(response.parsed_body["errors"]).to match_array(
|
||
[/Title is too short/, /Title seems unclear/],
|
||
)
|
||
end
|
||
|
||
it "returns errors when the rate limit is exceeded" do
|
||
EditRateLimiter
|
||
.any_instance
|
||
.expects(:performed!)
|
||
.raises(RateLimiter::LimitExceeded.new(60))
|
||
|
||
put "/t/#{topic.slug}/#{topic.id}.json",
|
||
params: {
|
||
title: "This is a new title for the topic",
|
||
}
|
||
|
||
expect(response.status).to eq(429)
|
||
end
|
||
|
||
it "returns errors with invalid categories" do
|
||
put "/t/#{topic.slug}/#{topic.id}.json", params: { category_id: -1 }
|
||
|
||
expect(response.status).to eq(422)
|
||
end
|
||
|
||
it "doesn't call the PostRevisor when there is no changes" do
|
||
expect do
|
||
put "/t/#{topic.slug}/#{topic.id}.json", params: { category_id: topic.category_id }
|
||
end.not_to change(PostRevision.all, :count)
|
||
|
||
expect(response.status).to eq(200)
|
||
end
|
||
|
||
describe "when first post is locked" do
|
||
it "blocks user from editing even if they are in 'edit_all_topic_groups' and 'edit_all_post_groups'" do
|
||
SiteSetting.edit_all_topic_groups = Group::AUTO_GROUPS[:trust_level_3]
|
||
SiteSetting.edit_all_post_groups = Group::AUTO_GROUPS[:trust_level_4]
|
||
user.update!(trust_level: 3)
|
||
topic.first_post.update!(locked_by_id: admin.id)
|
||
|
||
put "/t/#{topic.slug}/#{topic.id}.json", params: { title: topic.title + " hello" }
|
||
|
||
expect(response.status).to eq(403)
|
||
end
|
||
|
||
it "allows staff to edit" do
|
||
sign_in(Fabricate(:admin))
|
||
topic.first_post.update!(locked_by_id: admin.id)
|
||
|
||
put "/t/#{topic.slug}/#{topic.id}.json", params: { title: topic.title + " hello" }
|
||
expect(response.status).to eq(200)
|
||
end
|
||
end
|
||
|
||
context "with tags" do
|
||
before { SiteSetting.tagging_enabled = true }
|
||
|
||
describe "tagging by name" do
|
||
it "can add a tag to topic" do
|
||
expect do
|
||
put "/t/#{topic.slug}/#{topic.id}.json", params: { tags: [tag.name] }
|
||
end.to change { topic.reload.first_post.revisions.count }.by(1)
|
||
|
||
expect(response.status).to eq(200)
|
||
expect(topic.tags.pluck(:id)).to contain_exactly(tag.id)
|
||
end
|
||
|
||
it "can create a tag" do
|
||
SiteSetting.create_tag_allowed_groups = Group::AUTO_GROUPS[:trust_level_0]
|
||
expect do
|
||
put "/t/#{topic.slug}/#{topic.id}.json", params: { tags: ["newtag"] }
|
||
end.to change { topic.reload.first_post.revisions.count }.by(1)
|
||
|
||
expect(response.status).to eq(200)
|
||
expect(topic.reload.tags.pluck(:name)).to contain_exactly("newtag")
|
||
end
|
||
|
||
it "can change the category and create a new tag" do
|
||
SiteSetting.create_tag_allowed_groups = Group::AUTO_GROUPS[:trust_level_0]
|
||
expect do
|
||
put "/t/#{topic.slug}/#{topic.id}.json",
|
||
params: {
|
||
tags: ["newtag"],
|
||
category_id: category.id,
|
||
}
|
||
end.to change { topic.reload.first_post.revisions.count }.by(1)
|
||
|
||
expect(response.status).to eq(200)
|
||
expect(topic.reload.tags.pluck(:name)).to contain_exactly("newtag")
|
||
end
|
||
|
||
it "can add a tag to wiki topic" do
|
||
SiteSetting.edit_wiki_post_allowed_groups = Group::AUTO_GROUPS[:trust_level_2]
|
||
topic.first_post.update!(wiki: true)
|
||
sign_in(user_2)
|
||
|
||
expect do
|
||
put "/t/#{topic.id}/tags.json", params: { tags: [tag.name] }
|
||
end.not_to change { topic.reload.first_post.revisions.count }
|
||
|
||
expect(response.status).to eq(403)
|
||
user_2.groups << Group.find_by(name: "trust_level_2")
|
||
|
||
expect do put "/t/#{topic.id}/tags.json", params: { tags: [tag.name] } end.to change {
|
||
topic.reload.first_post.revisions.count
|
||
}.by(1)
|
||
|
||
expect(response.status).to eq(200)
|
||
expect(topic.tags.pluck(:id)).to contain_exactly(tag.id)
|
||
end
|
||
|
||
it "can remove a tag" do
|
||
topic.tags << tag
|
||
|
||
expect do
|
||
put "/t/#{topic.slug}/#{topic.id}.json", params: { tags: [""] }
|
||
end.to change { topic.reload.first_post.revisions.count }.by(1)
|
||
|
||
expect(response.status).to eq(200)
|
||
expect(topic.tags).to eq([])
|
||
end
|
||
|
||
it "does not cause a revision when tags have not changed" do
|
||
topic.tags << tag
|
||
|
||
expect do
|
||
put "/t/#{topic.slug}/#{topic.id}.json", params: { tags: [tag.name] }
|
||
end.not_to change { topic.reload.first_post.revisions.count }
|
||
|
||
expect(response.status).to eq(200)
|
||
end
|
||
|
||
it "returns canonical tags in the response when synonyms are submitted" do
|
||
canonical = Fabricate(:tag, name: "apple-inc")
|
||
Fabricate(:tag, name: "aapl", target_tag: canonical)
|
||
Fabricate(:tag, name: "appl", target_tag: canonical)
|
||
|
||
put "/t/#{topic.slug}/#{topic.id}.json", params: { tags: %w[aapl appl apple-inc] }
|
||
|
||
expect(response.status).to eq(200)
|
||
expect(response.parsed_body["tags"].map { |t| t["name"] }).to contain_exactly(
|
||
"apple-inc",
|
||
)
|
||
expect(topic.reload.tags.pluck(:name)).to contain_exactly("apple-inc")
|
||
end
|
||
|
||
it "does not include tags in the response when tags were not part of the update" do
|
||
put "/t/#{topic.slug}/#{topic.id}.json",
|
||
params: {
|
||
title: "This is a new title for the topic",
|
||
}
|
||
|
||
expect(response.status).to eq(200)
|
||
expect(response.parsed_body).not_to have_key("tags")
|
||
end
|
||
|
||
it "does not create a revision when only synonyms of existing tags are submitted" do
|
||
canonical = Fabricate(:tag, name: "apple-inc")
|
||
aapl = Fabricate(:tag, name: "aapl", target_tag: canonical)
|
||
appl = Fabricate(:tag, name: "appl", target_tag: canonical)
|
||
topic.tags << canonical
|
||
|
||
expect do
|
||
put "/t/#{topic.slug}/#{topic.id}.json",
|
||
params: {
|
||
tags: [
|
||
{ id: aapl.id, name: "aapl" },
|
||
{ id: appl.id, name: "appl" },
|
||
{ id: canonical.id, name: "apple-inc" },
|
||
],
|
||
}
|
||
end.not_to change { topic.reload.first_post.revisions.count }
|
||
|
||
expect(response.status).to eq(200)
|
||
expect(response.parsed_body["tags"].map { |t| t["name"] }).to contain_exactly(
|
||
"apple-inc",
|
||
)
|
||
end
|
||
end
|
||
|
||
it "returns success when updating with empty tags on a topic with no tags" do
|
||
expect(topic.tags).to be_empty
|
||
|
||
put "/t/#{topic.id}/tags.json"
|
||
|
||
expect(response.status).to eq(200)
|
||
expect(response.parsed_body["errors"]).to be_nil
|
||
end
|
||
|
||
it "can update tags" do
|
||
expect do
|
||
put "/t/#{topic.id}/tags.json", params: { tags: [{ id: tag.id, name: tag.name }] }
|
||
end.to change { topic.reload.first_post.revisions.count }.by(1)
|
||
|
||
expect(response.status).to eq(200)
|
||
expect(topic.tags.pluck(:id)).to contain_exactly(tag.id)
|
||
end
|
||
|
||
it "rejects tag arrays exceeding the configured per-topic limit" do
|
||
SiteSetting.max_tags_per_topic = 1
|
||
|
||
put "/t/#{topic.slug}/#{topic.id}.json",
|
||
params: {
|
||
tags: [{ id: tag.id, name: tag.name }, {}],
|
||
},
|
||
as: :json
|
||
|
||
expect(response.status).to eq(422)
|
||
expect(response.parsed_body["errors"]).to contain_exactly(
|
||
I18n.t("tags.too_many_tags_for_topic", count: 1),
|
||
)
|
||
expect(topic.reload.tags).to be_empty
|
||
end
|
||
|
||
it "can update tags when params are form-encoded as indexed hash" do
|
||
expect do
|
||
put "/t/#{topic.id}/tags.json",
|
||
params: {
|
||
tags: {
|
||
"0" => {
|
||
id: tag.id,
|
||
name: tag.name,
|
||
},
|
||
},
|
||
}
|
||
end.to change { topic.reload.first_post.revisions.count }.by(1)
|
||
|
||
expect(response.status).to eq(200)
|
||
expect(topic.tags.pluck(:id)).to contain_exactly(tag.id)
|
||
end
|
||
|
||
it "can create a new tag" do
|
||
SiteSetting.create_tag_allowed_groups = Group::AUTO_GROUPS[:trust_level_0]
|
||
|
||
expect do
|
||
put "/t/#{topic.id}/tags.json", params: { tags: [{ name: "brand-new" }] }
|
||
end.to change { topic.reload.first_post.revisions.count }.by(1)
|
||
|
||
expect(response.status).to eq(200)
|
||
expect(topic.reload.tags.pluck(:name)).to contain_exactly("brand-new")
|
||
end
|
||
|
||
it "returns canonical tags and skips revision when only synonyms are submitted" do
|
||
canonical = Fabricate(:tag, name: "apple-inc")
|
||
aapl = Fabricate(:tag, name: "aapl", target_tag: canonical)
|
||
appl = Fabricate(:tag, name: "appl", target_tag: canonical)
|
||
topic.tags << canonical
|
||
|
||
expect do
|
||
put "/t/#{topic.id}/tags.json",
|
||
params: {
|
||
tags: [
|
||
{ id: aapl.id, name: "aapl" },
|
||
{ id: appl.id, name: "appl" },
|
||
{ id: canonical.id, name: "apple-inc" },
|
||
],
|
||
}
|
||
end.not_to change { topic.reload.first_post.revisions.count }
|
||
|
||
expect(response.status).to eq(200)
|
||
expect(response.parsed_body["tags"].map { |t| t["name"] }).to contain_exactly(
|
||
"apple-inc",
|
||
)
|
||
end
|
||
|
||
it "does not remove tag if no params is given" do
|
||
topic.tags << tag
|
||
|
||
expect do put "/t/#{topic.slug}/#{topic.id}.json" end.to_not change {
|
||
topic.reload.tags.count
|
||
}
|
||
|
||
expect(response.status).to eq(200)
|
||
end
|
||
|
||
it "does not cause a revision when tags have not changed" do
|
||
topic.tags << tag
|
||
|
||
expect do
|
||
put "/t/#{topic.slug}/#{topic.id}.json",
|
||
params: {
|
||
tags: [{ id: tag.id, name: tag.name }],
|
||
}
|
||
end.not_to change { topic.reload.first_post.revisions.count }
|
||
|
||
expect(response.status).to eq(200)
|
||
end
|
||
|
||
it "can add a tag on topic update" do
|
||
SiteSetting.create_tag_allowed_groups = Group::AUTO_GROUPS[:trust_level_0]
|
||
topic.tags << tag
|
||
|
||
expect do
|
||
put "/t/#{topic.slug}/#{topic.id}.json",
|
||
params: {
|
||
tags: [{ id: tag.id, name: tag.name }, { name: "new-tag" }],
|
||
}
|
||
end.to change { topic.reload.first_post.revisions.count }.by(1)
|
||
|
||
expect(response.status).to eq(200)
|
||
expect(topic.reload.tags.pluck(:name)).to contain_exactly(tag.name, "new-tag")
|
||
end
|
||
|
||
it "can remove a tag on topic update" do
|
||
tag2 = Fabricate(:tag)
|
||
topic.tags << tag
|
||
topic.tags << tag2
|
||
|
||
expect do
|
||
put "/t/#{topic.slug}/#{topic.id}.json",
|
||
params: {
|
||
tags: [{ id: tag.id, name: tag.name }],
|
||
}
|
||
end.to change { topic.reload.first_post.revisions.count }.by(1)
|
||
|
||
expect(response.status).to eq(200)
|
||
expect(topic.reload.tags).to contain_exactly(tag)
|
||
end
|
||
|
||
it "does not create a revision when tags param is empty and topic has no tags" do
|
||
expect do
|
||
put "/t/#{topic.slug}/#{topic.id}.json", params: { tags: [] }, as: :json
|
||
end.not_to change { topic.reload.first_post.revisions.count }
|
||
|
||
expect(response.status).to eq(200)
|
||
end
|
||
|
||
it "creates a revision when all tags are removed from a topic" do
|
||
topic.tags << tag
|
||
|
||
expect do
|
||
put "/t/#{topic.slug}/#{topic.id}.json", params: { tags: [] }, as: :json
|
||
end.to change { topic.reload.first_post.revisions.count }.by(1)
|
||
|
||
expect(response.status).to eq(200)
|
||
expect(topic.reload.tags).to be_empty
|
||
end
|
||
end
|
||
|
||
context "when topic is private" do
|
||
before do
|
||
topic.update!(
|
||
archetype: Archetype.private_message,
|
||
category: nil,
|
||
allowed_users: [topic.user],
|
||
)
|
||
end
|
||
|
||
context "when there are no changes" do
|
||
it "does not call the PostRevisor" do
|
||
expect do
|
||
put "/t/#{topic.slug}/#{topic.id}.json", params: { category_id: topic.category_id }
|
||
end.not_to change(PostRevision.all, :count)
|
||
|
||
expect(response.status).to eq(200)
|
||
end
|
||
end
|
||
end
|
||
|
||
context "when updating to a category with restricted tags" do
|
||
fab!(:restricted_category, :category)
|
||
fab!(:tag1, :tag)
|
||
fab!(:tag2, :tag)
|
||
fab!(:tag3, :tag)
|
||
fab!(:tag_group_1) { Fabricate(:tag_group, tag_names: [tag1.name]) }
|
||
fab!(:tag_group_2, :tag_group)
|
||
|
||
before_all do
|
||
SiteSetting.tagging_enabled = true
|
||
topic.update!(tags: [tag1])
|
||
end
|
||
|
||
it "can’t change to a category disallowing this topic current tags" do
|
||
restricted_category.allowed_tags = [tag2.name]
|
||
|
||
put "/t/#{topic.slug}/#{topic.id}.json", params: { category_id: restricted_category.id }
|
||
|
||
result = response.parsed_body
|
||
|
||
expect(response.status).to eq(422)
|
||
expect(result["errors"]).to be_present
|
||
expect(topic.reload.category_id).not_to eq(restricted_category.id)
|
||
end
|
||
|
||
it "can’t change to a category disallowing this topic current tag (through tag_group)" do
|
||
tag_group_2.tags = [tag2]
|
||
restricted_category.allowed_tag_groups = [tag_group_2.name]
|
||
|
||
put "/t/#{topic.slug}/#{topic.id}.json", params: { category_id: restricted_category.id }
|
||
|
||
result = response.parsed_body
|
||
|
||
expect(response.status).to eq(422)
|
||
expect(result["errors"]).to be_present
|
||
expect(topic.reload.category_id).not_to eq(restricted_category.id)
|
||
end
|
||
|
||
it "can change to a category allowing this topic current tags" do
|
||
restricted_category.allowed_tags = [tag1.name]
|
||
|
||
put "/t/#{topic.slug}/#{topic.id}.json", params: { category_id: restricted_category.id }
|
||
|
||
expect(response.status).to eq(200)
|
||
end
|
||
|
||
it "can change to a category allowing this topic current tags (through tag_group)" do
|
||
tag_group_1.tags = [tag1]
|
||
restricted_category.allowed_tag_groups = [tag_group_1.name]
|
||
|
||
put "/t/#{topic.slug}/#{topic.id}.json", params: { category_id: restricted_category.id }
|
||
|
||
expect(response.status).to eq(200)
|
||
end
|
||
|
||
it "can change to a category allowing any tag" do
|
||
put "/t/#{topic.slug}/#{topic.id}.json", params: { category_id: category.id }
|
||
|
||
expect(response.status).to eq(200)
|
||
end
|
||
|
||
it "can’t add a category-only tags from another category to a category" do
|
||
restricted_category.allowed_tags = [tag2.name]
|
||
|
||
put "/t/#{topic.slug}/#{topic.id}.json",
|
||
params: {
|
||
tags: [tag2.name],
|
||
category_id: category.id,
|
||
}
|
||
|
||
result = response.parsed_body
|
||
expect(response.status).to eq(422)
|
||
expect(result["errors"]).to be_present
|
||
expect(result["errors"][0]).to include(tag2.name)
|
||
expect(topic.reload.category_id).not_to eq(restricted_category.id)
|
||
end
|
||
|
||
it "allows category change when topic has a hidden tag" do
|
||
Fabricate(:tag_group, permissions: { "staff" => 1 }, tag_names: [tag1.name])
|
||
|
||
put "/t/#{topic.slug}/#{topic.id}.json", params: { category_id: category.id }
|
||
|
||
expect(response.status).to eq(200)
|
||
expect(topic.reload.tags).to include(tag1)
|
||
end
|
||
|
||
it "allows category change when topic has a read-only tag" do
|
||
Fabricate(
|
||
:tag_group,
|
||
permissions: {
|
||
"staff" => 1,
|
||
"everyone" => 3,
|
||
},
|
||
tag_names: [tag3.name],
|
||
)
|
||
topic.update!(tags: [tag3])
|
||
|
||
put "/t/#{topic.slug}/#{topic.id}.json", params: { category_id: category.id }
|
||
|
||
expect(response.status).to eq(200)
|
||
expect(topic.reload.tags).to contain_exactly(tag3)
|
||
end
|
||
|
||
it "does not leak tag name when trying to use a staff tag" do
|
||
Fabricate(:tag_group, permissions: { "staff" => 1 }, tag_names: [tag3.name])
|
||
|
||
put "/t/#{topic.slug}/#{topic.id}.json",
|
||
params: {
|
||
tags: [tag3.name],
|
||
category_id: category.id,
|
||
}
|
||
|
||
result = response.parsed_body
|
||
expect(response.status).to eq(422)
|
||
expect(result["errors"]).to be_present
|
||
expect(result["errors"][0]).not_to include(tag3.name)
|
||
end
|
||
|
||
it "does not resolve hidden tags sent by ID" do
|
||
Fabricate(:tag_group, permissions: { "staff" => 1 }, tag_names: [tag3.name])
|
||
restricted_category.allowed_tags = [tag2.name]
|
||
|
||
put "/t/#{topic.slug}/#{topic.id}.json",
|
||
params: {
|
||
tags: [{ id: tag2.id, name: tag2.name }, { id: tag3.id, name: "anything" }],
|
||
category_id: restricted_category.id,
|
||
}
|
||
|
||
expect(response.status).to eq(200)
|
||
expect(topic.reload.tags.map(&:name)).not_to include(tag3.name)
|
||
end
|
||
|
||
it "will clean tag params" do
|
||
restricted_category.allowed_tags = [tag2.name]
|
||
|
||
put "/t/#{topic.slug}/#{topic.id}.json",
|
||
params: {
|
||
tags: [""],
|
||
category_id: restricted_category.id,
|
||
}
|
||
|
||
expect(response.status).to eq(200)
|
||
end
|
||
|
||
context "with content localization enabled" do
|
||
before do
|
||
SiteSetting.content_localization_enabled = true
|
||
SiteSetting.content_localization_supported_locales = "en|ja"
|
||
tag1.update!(locale: "en")
|
||
Fabricate(:tag_localization, tag: tag1, locale: "ja", name: "タグ1")
|
||
user.update!(locale: "ja")
|
||
end
|
||
|
||
it "can change category with localized tags" do
|
||
restricted_category.allowed_tags = [tag1.name]
|
||
|
||
put "/t/#{topic.slug}/#{topic.id}.json",
|
||
params: {
|
||
tags: [{ id: tag1.id, name: "タグ1" }],
|
||
category_id: restricted_category.id,
|
||
}
|
||
|
||
expect(response.status).to eq(200)
|
||
end
|
||
|
||
it "can change category when tags are sent as strings" do
|
||
restricted_category.allowed_tags = [tag1.name]
|
||
|
||
put "/t/#{topic.slug}/#{topic.id}.json",
|
||
params: {
|
||
tags: [tag1.name],
|
||
category_id: restricted_category.id,
|
||
}
|
||
|
||
expect(response.status).to eq(200)
|
||
end
|
||
|
||
it "can edit tags with localized tag names" do
|
||
tag2 = Fabricate(:tag, name: "planning", locale: "en")
|
||
Fabricate(:tag_localization, tag: tag2, locale: "ja", name: "計画")
|
||
|
||
put "/t/#{topic.slug}/#{topic.id}.json",
|
||
params: {
|
||
tags: [{ id: tag1.id, name: "タグ1" }, { id: tag2.id, name: "計画" }],
|
||
}
|
||
|
||
expect(response.status).to eq(200)
|
||
expect(topic.reload.tags).to contain_exactly(tag1, tag2)
|
||
end
|
||
end
|
||
end
|
||
|
||
context "when allow_uncategorized_topics is false" do
|
||
before { SiteSetting.allow_uncategorized_topics = false }
|
||
|
||
it "can add a category to an uncategorized topic" do
|
||
put "/t/#{topic.slug}/#{topic.id}.json", params: { category_id: category.id }
|
||
|
||
expect(response.status).to eq(200)
|
||
expect(topic.reload.category).to eq(category)
|
||
end
|
||
end
|
||
end
|
||
end
|
||
|
||
describe "featured links" do
|
||
def fabricate_topic(user, category = nil)
|
||
topic = Fabricate(:topic, user: user, category: category)
|
||
Fabricate(:post, user: post_author1, topic: topic)
|
||
topic
|
||
end
|
||
|
||
it "allows to update topic featured link" do
|
||
sign_in(trust_level_1)
|
||
|
||
tl1_topic = fabricate_topic(trust_level_1)
|
||
put "/t/#{tl1_topic.slug}/#{tl1_topic.id}.json",
|
||
params: {
|
||
featured_link: "https://discourse.org",
|
||
}
|
||
|
||
expect(response.status).to eq(200)
|
||
end
|
||
|
||
it "doesn't allow TL0 users to update topic featured link" do
|
||
sign_in(trust_level_0)
|
||
|
||
tl0_topic = fabricate_topic(trust_level_0)
|
||
put "/t/#{tl0_topic.slug}/#{tl0_topic.id}.json",
|
||
params: {
|
||
featured_link: "https://discourse.org",
|
||
}
|
||
|
||
expect(response.status).to eq(422)
|
||
end
|
||
|
||
it "doesn't allow to update topic featured link if featured links are disabled in settings" do
|
||
sign_in(trust_level_1)
|
||
|
||
SiteSetting.topic_featured_link_enabled = false
|
||
tl1_topic = fabricate_topic(trust_level_1)
|
||
put "/t/#{tl1_topic.slug}/#{tl1_topic.id}.json",
|
||
params: {
|
||
featured_link: "https://discourse.org",
|
||
}
|
||
|
||
expect(response.status).to eq(422)
|
||
end
|
||
|
||
it "doesn't allow to update topic featured link in the category with forbidden feature links" do
|
||
sign_in(trust_level_1)
|
||
|
||
category = Fabricate(:category, topic_featured_link_allowed: false)
|
||
tl1_topic_in_category = fabricate_topic(trust_level_1, category)
|
||
put "/t/#{tl1_topic_in_category.slug}/#{tl1_topic_in_category.id}.json",
|
||
params: {
|
||
featured_link: "https://discourse.org",
|
||
}
|
||
|
||
expect(response.status).to eq(422)
|
||
end
|
||
end
|
||
end
|
||
|
||
describe "#show_by_external_id" do
|
||
fab!(:private_topic) { Fabricate(:private_message_topic, external_id: "private") }
|
||
fab!(:topic) { Fabricate(:topic, external_id: "asdf") }
|
||
|
||
it "returns 301 when found" do
|
||
get "/t/external_id/asdf.json"
|
||
expect(response.status).to eq(301)
|
||
expect(response).to redirect_to(topic.relative_url + ".json")
|
||
end
|
||
|
||
it "returns right response when not found" do
|
||
get "/t/external_id/fdsa.json"
|
||
expect(response.status).to eq(404)
|
||
end
|
||
|
||
it "preserves only select query params" do
|
||
get "/t/external_id/asdf.json", params: { filter_top_level_replies: true }
|
||
expect(response.status).to eq(301)
|
||
expect(response).to redirect_to("#{topic.relative_url}.json?filter_top_level_replies=true")
|
||
|
||
get "/t/external_id/asdf.json", params: { not_valid: true }
|
||
expect(response.status).to eq(301)
|
||
expect(response).to redirect_to(topic.relative_url + ".json")
|
||
|
||
get "/t/external_id/asdf.json", params: { filter_top_level_replies: true, post_number: 9999 }
|
||
expect(response.status).to eq(301)
|
||
expect(response).to redirect_to(
|
||
"#{topic.relative_url}/9999.json?filter_top_level_replies=true",
|
||
)
|
||
|
||
get "/t/external_id/asdf.json",
|
||
params: {
|
||
filter_top_level_replies: true,
|
||
print: true,
|
||
preview_theme_id: 9999,
|
||
include_raw: true,
|
||
}
|
||
expect(response.status).to eq(301)
|
||
expect(response).to redirect_to(
|
||
"#{topic.relative_url}.json?print=true&filter_top_level_replies=true&preview_theme_id=9999&include_raw=true",
|
||
)
|
||
end
|
||
|
||
describe "when user does not have access to the topic" do
|
||
it "should return the right response" do
|
||
sign_in(user)
|
||
|
||
get "/t/external_id/private.json"
|
||
|
||
expect(response.status).to eq(403)
|
||
expect(response.body).to include(I18n.t("invalid_access"))
|
||
end
|
||
end
|
||
end
|
||
|
||
describe "#show" do
|
||
fab!(:private_topic) { pm }
|
||
fab!(:topic) { Fabricate(:post, user: post_author1).topic }
|
||
|
||
describe "when topic is not allowed" do
|
||
it "should return the right response" do
|
||
SiteSetting.detailed_404 = true
|
||
sign_in(user)
|
||
|
||
get "/t/#{private_topic.id}.json"
|
||
|
||
expect(response.status).to eq(403)
|
||
expect(response.body).to include(I18n.t("invalid_access"))
|
||
end
|
||
end
|
||
|
||
describe "when topic is allowed to a group" do
|
||
fab!(:group) { Fabricate(:group, public_admission: true) }
|
||
fab!(:category) do
|
||
Fabricate(:category_with_definition).tap do |category|
|
||
category.set_permissions(group => :full)
|
||
category.save!
|
||
end
|
||
end
|
||
fab!(:topic) { Fabricate(:topic, category: category) }
|
||
|
||
before { SiteSetting.detailed_404 = true }
|
||
|
||
it "shows a descriptive error message containing the group name" do
|
||
get "/t/#{topic.id}.json"
|
||
|
||
html = CGI.unescapeHTML(response.parsed_body["extras"]["html"])
|
||
expect(response.status).to eq(403)
|
||
expect(html).to include(I18n.t("not_in_group.title_topic", group: group.name))
|
||
expect(html).to include(I18n.t("not_in_group.join_group"))
|
||
end
|
||
end
|
||
|
||
it "correctly renders canonicals" do
|
||
get "/t/#{topic.id}", params: { slug: topic.slug }
|
||
|
||
expect(response.status).to eq(200)
|
||
expect(css_select("link[rel=canonical]").length).to eq(1)
|
||
expect(response.headers["Cache-Control"]).to eq("no-cache, no-store")
|
||
end
|
||
|
||
it "returns 301 even if slug does not match URL" do
|
||
# in the past we had special logic for unlisted topics
|
||
# we would require slug unless you made a json call
|
||
# this was not really providing any security
|
||
#
|
||
# we no longer require a topic be visible to perform url correction
|
||
# if you need to properly hide a topic for users use a secure category
|
||
# or a PM
|
||
Fabricate(:post, user: post_author1, topic: invisible_topic)
|
||
|
||
get "/t/#{invisible_topic.id}.json", params: { slug: invisible_topic.slug }
|
||
expect(response.status).to eq(200)
|
||
|
||
get "/t/#{topic.id}.json", params: { slug: "just-guessing" }
|
||
expect(response.status).to eq(301)
|
||
|
||
get "/t/#{topic.slug}.json"
|
||
expect(response.status).to eq(301)
|
||
end
|
||
|
||
it "shows a topic correctly" do
|
||
get "/t/#{topic.slug}/#{topic.id}.json"
|
||
expect(response.status).to eq(200)
|
||
end
|
||
|
||
it "does not expose private message tag descriptions when the viewer cannot see tags" do
|
||
SiteSetting.tagging_enabled = true
|
||
SiteSetting.pm_tags_allowed_for_groups = Group::AUTO_GROUPS[:admins].to_s
|
||
pm_post = Fabricate(:private_message_post, user: admin, recipient: user)
|
||
pm_topic = pm_post.topic
|
||
pm_tag = Fabricate(:tag, name: "secret-pm-tag", description: "secret PM tag description")
|
||
pm_topic.tags << pm_tag
|
||
|
||
sign_in(user)
|
||
get "/t/#{pm_topic.slug}/#{pm_topic.id}.json"
|
||
|
||
expect(response.status).to eq(200)
|
||
expect(response.parsed_body).not_to have_key("tags")
|
||
expect(response.parsed_body).not_to have_key("tags_descriptions")
|
||
end
|
||
|
||
it "does not expose links from hidden posts in topic details to non-staff viewers" do
|
||
test_topic = Fabricate(:topic, user: post_author1)
|
||
visible_post = Fabricate(:post, topic: test_topic, user: post_author1)
|
||
hidden_post = Fabricate(:post, topic: test_topic, user: post_author1)
|
||
|
||
Fabricate(
|
||
:topic_link,
|
||
post: visible_post,
|
||
url: "https://visible-link.example.com",
|
||
domain: "visible-link.example.com",
|
||
clicks: 1,
|
||
title: "Visible title",
|
||
)
|
||
Fabricate(
|
||
:topic_link,
|
||
post: hidden_post,
|
||
url: "https://hidden-link.example.com",
|
||
domain: "hidden-link.example.com",
|
||
clicks: 1,
|
||
title: "Hidden title",
|
||
)
|
||
|
||
hidden_post.hide!(PostActionType.types[:off_topic])
|
||
|
||
get "/t/#{test_topic.slug}/#{test_topic.id}.json"
|
||
|
||
expect(response.status).to eq(200)
|
||
expect(response.parsed_body["details"]["links"]).to contain_exactly(
|
||
a_hash_including("url" => "https://visible-link.example.com", "title" => "Visible title"),
|
||
)
|
||
|
||
sign_in(moderator)
|
||
get "/t/#{test_topic.slug}/#{test_topic.id}.json"
|
||
|
||
expect(response.status).to eq(200)
|
||
expect(response.parsed_body["details"]["links"]).to contain_exactly(
|
||
a_hash_including("url" => "https://visible-link.example.com", "title" => "Visible title"),
|
||
a_hash_including("url" => "https://hidden-link.example.com", "title" => "Hidden title"),
|
||
)
|
||
end
|
||
|
||
it "does not expose hidden post link counts", :aggregate_failures do
|
||
first_post = Fabricate(:post, user: post_author1)
|
||
test_topic = first_post.topic
|
||
visible_post = Fabricate(:post, topic: test_topic, user: post_author1)
|
||
hidden_post =
|
||
Fabricate(
|
||
:post,
|
||
topic: test_topic,
|
||
user: post_author1,
|
||
hidden: true,
|
||
hidden_reason_id: Post.hidden_reasons[:flag_threshold_reached],
|
||
)
|
||
|
||
visible_link =
|
||
Fabricate(
|
||
:topic_link,
|
||
post: visible_post,
|
||
url: "https://visible-link-count.example.com",
|
||
domain: "visible-link-count.example.com",
|
||
title: "Visible link count title",
|
||
)
|
||
hidden_link =
|
||
Fabricate(
|
||
:topic_link,
|
||
post: hidden_post,
|
||
url: "https://hidden-link-count.example.com",
|
||
domain: "hidden-link-count.example.com",
|
||
title: "Hidden link count title",
|
||
)
|
||
|
||
sign_in(user_2)
|
||
|
||
get "/t/#{test_topic.slug}/#{test_topic.id}.json"
|
||
|
||
expect(response.status).to eq(200)
|
||
show_posts = response.parsed_body.dig("post_stream", "posts")
|
||
show_hidden_post = show_posts.find { |post| post["id"] == hidden_post.id }
|
||
show_visible_post = show_posts.find { |post| post["id"] == visible_post.id }
|
||
expect(show_hidden_post).to include("hidden" => true, "can_see_hidden_post" => false)
|
||
expect(show_hidden_post).not_to have_key("link_counts")
|
||
expect(show_visible_post["link_counts"]).to contain_exactly(
|
||
a_hash_including("url" => visible_link.url, "title" => visible_link.title),
|
||
)
|
||
expect(response.body).not_to include(hidden_link.url)
|
||
|
||
get "/t/#{test_topic.id}/posts.json", params: { post_ids: [hidden_post.id, visible_post.id] }
|
||
|
||
expect(response.status).to eq(200)
|
||
posts = response.parsed_body.dig("post_stream", "posts")
|
||
posts_hidden_post = posts.find { |post| post["id"] == hidden_post.id }
|
||
posts_visible_post = posts.find { |post| post["id"] == visible_post.id }
|
||
expect(posts_hidden_post).to include("hidden" => true, "can_see_hidden_post" => false)
|
||
expect(posts_hidden_post).not_to have_key("link_counts")
|
||
expect(posts_visible_post["link_counts"]).to contain_exactly(
|
||
a_hash_including("url" => visible_link.url, "title" => visible_link.title),
|
||
)
|
||
expect(response.body).not_to include(hidden_link.url)
|
||
end
|
||
|
||
it "shows a blank-slug topic without redirecting" do
|
||
topic.update_columns(title: "", slug: nil)
|
||
topic.reload
|
||
|
||
get "/t/#{topic.id}"
|
||
|
||
expect(response.status).to eq(200)
|
||
end
|
||
|
||
it "redirects an over-range page to the last valid page" do
|
||
get "/t/#{topic.slug}/#{topic.id}.json", params: { page: 2 }
|
||
expect(response).to redirect_to("/t/#{topic.slug}/#{topic.id}.json")
|
||
end
|
||
|
||
it "redirects over-range pages to the last multi-page page" do
|
||
topic_with_posts = Fabricate(:topic)
|
||
Fabricate.times(25, :post, topic: topic_with_posts)
|
||
Topic.reset_highest(topic_with_posts.id)
|
||
|
||
get "/t/#{topic_with_posts.slug}/#{topic_with_posts.id}", params: { page: 5 }
|
||
expect(response).to redirect_to("/t/#{topic_with_posts.slug}/#{topic_with_posts.id}?page=2")
|
||
end
|
||
|
||
it "uses viewer-visible post count when deciding the last valid page (whispers)" do
|
||
SiteSetting.whispers_allowed_groups = "#{Group::AUTO_GROUPS[:staff]}"
|
||
|
||
topic_with_posts = Fabricate(:topic)
|
||
Fabricate.times(20, :post, topic: topic_with_posts)
|
||
Fabricate(:post, topic: topic_with_posts, post_type: Post.types[:whisper])
|
||
Topic.reset_highest(topic_with_posts.id)
|
||
|
||
get "/t/#{topic_with_posts.slug}/#{topic_with_posts.id}.json", params: { page: 2 }
|
||
expect(response).to redirect_to("/t/#{topic_with_posts.slug}/#{topic_with_posts.id}.json")
|
||
|
||
sign_in(admin)
|
||
get "/t/#{topic_with_posts.slug}/#{topic_with_posts.id}.json", params: { page: 2 }
|
||
expect(response.status).to eq(200)
|
||
end
|
||
|
||
it "can find a topic given a slug in the id param" do
|
||
get "/t/#{topic.slug}"
|
||
expect(response).to redirect_to(topic.relative_url)
|
||
end
|
||
|
||
it "can find a topic when a slug has a number in front" do
|
||
another_topic = Fabricate(:post, user: post_author1).topic
|
||
|
||
topic.update_column(:slug, "#{another_topic.id}-reasons-discourse-is-awesome")
|
||
get "/t/#{another_topic.id}-reasons-discourse-is-awesome"
|
||
|
||
expect(response).to redirect_to(topic.relative_url)
|
||
end
|
||
|
||
it "does not raise an unhandled exception when receiving an array of IDs" do
|
||
get "/t/#{topic.id}/summary?id[]=a,b"
|
||
|
||
expect(response.status).to eq(400)
|
||
end
|
||
|
||
it "does not raise an unhandled exception when receiving a nested ID parameter" do
|
||
get "/t/#{topic.id}/summary?id[foo]=a"
|
||
|
||
expect(response.status).to eq(400)
|
||
end
|
||
|
||
it "keeps the post_number parameter around when redirecting" do
|
||
get "/t/#{topic.slug}", params: { post_number: 42 }
|
||
expect(response).to redirect_to(topic.relative_url + "/42")
|
||
end
|
||
|
||
it "keeps the page around when redirecting" do
|
||
get "/t/#{topic.slug}", params: { post_number: 42, page: 123 }
|
||
|
||
expect(response).to redirect_to(topic.relative_url + "/42?page=123")
|
||
end
|
||
|
||
it "does not accept page params as an array" do
|
||
get "/t/#{topic.slug}", params: { post_number: 42, page: [2] }
|
||
|
||
expect(response).to redirect_to("#{topic.relative_url}/42?page=1")
|
||
end
|
||
|
||
it "scrubs invalid query parameters when redirecting" do
|
||
get "/t/#{topic.slug}", params: { silly_param: "hehe" }
|
||
|
||
expect(response).to redirect_to(topic.relative_url)
|
||
end
|
||
|
||
it "serves the topic route when nested_replies_default is enabled" do
|
||
SiteSetting.nested_replies_enabled = true
|
||
SiteSetting.nested_replies_default = true
|
||
|
||
get "/t/#{topic.slug}/#{topic.id}"
|
||
|
||
expect(response.status).to eq(200)
|
||
end
|
||
|
||
it "does not redirect crawlers to nested view" do
|
||
SiteSetting.nested_replies_enabled = true
|
||
SiteSetting.nested_replies_default = true
|
||
|
||
get "/t/#{topic.slug}/#{topic.id}", headers: { "HTTP_USER_AGENT" => "Googlebot" }
|
||
|
||
expect(response.status).to eq(200)
|
||
expect(response.body).to have_tag(:body, with: { class: "crawler" })
|
||
end
|
||
|
||
it "does not redirect private messages to nested view" do
|
||
SiteSetting.nested_replies_enabled = true
|
||
SiteSetting.nested_replies_default = true
|
||
pm = Fabricate(:private_message_topic, user: user)
|
||
Fabricate(:post, topic: pm, user: user)
|
||
|
||
sign_in(user)
|
||
get "/t/#{pm.slug}/#{pm.id}"
|
||
|
||
expect(response).not_to redirect_to("/n/#{pm.slug}/#{pm.id}")
|
||
end
|
||
|
||
it "serves embed_mode on the topic route for nested topics" do
|
||
SiteSetting.nested_replies_enabled = true
|
||
SiteSetting.nested_replies_default = true
|
||
|
||
get "/t/#{topic.slug}/#{topic.id}", params: { embed_mode: "true" }
|
||
|
||
expect(response.status).to eq(200)
|
||
end
|
||
|
||
it "serves embed class_name on the topic route for nested topics" do
|
||
SiteSetting.nested_replies_enabled = true
|
||
SiteSetting.nested_replies_default = true
|
||
|
||
get "/t/#{topic.slug}/#{topic.id}", params: { embed_mode: "true", class_name: "lee-af" }
|
||
|
||
expect(response.status).to eq(200)
|
||
end
|
||
|
||
it "returns 404 when an invalid slug is given and no id" do
|
||
get "/t/nope-nope.json"
|
||
|
||
expect(response.status).to eq(404)
|
||
end
|
||
|
||
it "returns a 404 when slug and topic id do not match a topic" do
|
||
get "/t/made-up-topic-slug/123456.json"
|
||
expect(response.status).to eq(404)
|
||
end
|
||
|
||
it "returns a 404 for an ID that is larger than postgres limits" do
|
||
get "/t/made-up-topic-slug/5014217323220164041.json"
|
||
|
||
expect(response.status).to eq(404)
|
||
end
|
||
|
||
it "doesn't use print mode when print equals false" do
|
||
SiteSetting.max_prints_per_hour_per_user = 0
|
||
|
||
get "/t/#{topic.slug}/#{topic.id}.json?print=false"
|
||
expect(response.status).to eq(200)
|
||
end
|
||
|
||
it "does not result in N+1 queries problem when multiple topic participants have primary or flair group configured" do
|
||
Group.user_trust_level_change!(post_author1.id, post_author1.trust_level)
|
||
user2 = Fabricate(:user)
|
||
user3 = Fabricate(:user)
|
||
_post2 = Fabricate(:post, topic: topic, user: user2)
|
||
_post3 = Fabricate(:post, topic: topic, user: user3)
|
||
group = Fabricate(:group)
|
||
user2.update!(primary_group: group)
|
||
user3.update!(flair_group: group)
|
||
|
||
# warm up
|
||
get "/t/#{topic.id}.json"
|
||
expect(response.status).to eq(200)
|
||
|
||
first_request_queries =
|
||
track_sql_queries do
|
||
get "/t/#{topic.id}.json"
|
||
|
||
expect(response.status).to eq(200)
|
||
|
||
expect(
|
||
response.parsed_body["details"]["participants"].map { |u| u["id"] },
|
||
).to contain_exactly(post_author1.id, user2.id, user3.id)
|
||
end
|
||
|
||
group2 = Fabricate(:group)
|
||
user4 = Fabricate(:user, flair_group: group2)
|
||
user5 = Fabricate(:user, primary_group: group2)
|
||
_post4 = Fabricate(:post, topic: topic, user: user4)
|
||
_post5 = Fabricate(:post, topic: topic, user: user5)
|
||
|
||
second_request_queries =
|
||
track_sql_queries do
|
||
get "/t/#{topic.id}.json"
|
||
|
||
expect(response.status).to eq(200)
|
||
|
||
expect(
|
||
response.parsed_body["details"]["participants"].map { |u| u["id"] },
|
||
).to contain_exactly(post_author1.id, user2.id, user3.id, user4.id, user5.id)
|
||
end
|
||
|
||
expect(second_request_queries.count).to eq(first_request_queries.count)
|
||
end
|
||
|
||
it "does not result in N+1 queries loading mentioned users" do
|
||
SiteSetting.enable_user_status = true
|
||
|
||
post =
|
||
Fabricate(
|
||
:post,
|
||
raw:
|
||
"post with many mentions: @#{user.username}, @#{user_2.username}, @#{admin.username}, @#{moderator.username}",
|
||
)
|
||
|
||
queries = track_sql_queries { get "/t/#{post.topic_id}.json" }
|
||
|
||
user_statuses_queries = queries.filter { |q| q =~ /FROM "?user_statuses"?/ }
|
||
expect(user_statuses_queries.size).to eq(2) # for current user and for all mentioned users
|
||
|
||
user_options_queries = queries.filter { |q| q =~ /FROM "?user_options"?/ }
|
||
expect(user_options_queries.size).to eq(1) # for all mentioned users
|
||
end
|
||
|
||
context "when content_localization_enabled true" do
|
||
before do
|
||
SiteSetting.content_localization_enabled = true
|
||
SiteSetting.content_localization_allowed_groups = Group::AUTO_GROUPS[:everyone]
|
||
end
|
||
|
||
it "does not result in N+1 queries when loading a localized post" do
|
||
3.times do
|
||
Fabricate(:post_localization, post: Fabricate(:post, topic:, locale: "ja"), locale: "en")
|
||
end
|
||
|
||
queries =
|
||
track_sql_queries do
|
||
sign_in(admin)
|
||
get "/t/#{topic.slug}/#{topic.id}.json"
|
||
end
|
||
|
||
queries = queries.filter { |q| q =~ /FROM "?post_localizations"?/ }
|
||
expect(queries.size).to eq(1)
|
||
end
|
||
|
||
context "with an internal topic onebox" do
|
||
fab!(:reader) { Fabricate(:user, locale: "ja") }
|
||
fab!(:onebox_topic) { Fabricate(:topic, title: "Sun Tzu's strategies", locale: "en") }
|
||
fab!(:onebox_post) do
|
||
Fabricate(:post, topic: onebox_topic, post_number: 1, locale: "en", raw: "Subdue them.")
|
||
end
|
||
fab!(:host_topic) { Fabricate(:topic, locale: "ja") }
|
||
fab!(:host_post) do
|
||
Fabricate(:post, topic: host_topic, post_number: 1, locale: "ja", raw: "見てください")
|
||
end
|
||
|
||
before do
|
||
SiteSetting.allow_user_locale = true
|
||
SiteSetting.content_localization_supported_locales = "en|ja"
|
||
Fabricate(:topic_localization, topic: onebox_topic, locale: "ja", title: "孫子の兵法")
|
||
Fabricate(:post_localization, post: onebox_post, locale: "ja", cooked: "<p>戦わずして勝つ</p>")
|
||
TopicLink.create!(
|
||
topic: host_topic,
|
||
post: host_post,
|
||
user: host_post.user,
|
||
url: onebox_post.url,
|
||
domain: Discourse.current_hostname,
|
||
internal: true,
|
||
quote: true,
|
||
reflection: false,
|
||
link_topic_id: onebox_topic.id,
|
||
link_post_id: onebox_post.id,
|
||
)
|
||
end
|
||
|
||
def host_post_json
|
||
get "/t/#{host_topic.id}.json"
|
||
expect(response.status).to eq(200)
|
||
response.parsed_body["post_stream"]["posts"].find { |p| p["id"] == host_post.id }
|
||
end
|
||
|
||
it "returns localized onebox data for a reader in their own language" do
|
||
sign_in(reader)
|
||
|
||
entry = host_post_json["localized_oneboxes"].first
|
||
expect(entry["title"]).to eq("孫子の兵法")
|
||
expect(entry["excerpt"]).to include("戦わずして勝つ")
|
||
end
|
||
|
||
it "omits localized onebox data when the reader chose to see original content" do
|
||
reader.user_option.update!(show_original_content: true)
|
||
sign_in(reader)
|
||
|
||
expect(host_post_json.key?("localized_oneboxes")).to eq(false)
|
||
end
|
||
end
|
||
end
|
||
|
||
context "with serialize_post_user_badges" do
|
||
fab!(:badge)
|
||
before do
|
||
theme = Fabricate(:theme)
|
||
theme.theme_modifier_set.update!(serialize_post_user_badges: [badge.name])
|
||
SiteSetting.default_theme_id = theme.id
|
||
end
|
||
|
||
it "correctly returns user badges that are registered" do
|
||
first_post = topic.posts.order(:post_number).first
|
||
first_post.user.user_badges.create!(
|
||
badge_id: badge.id,
|
||
granted_at: Time.zone.now,
|
||
granted_by: Discourse.system_user,
|
||
)
|
||
|
||
expected_payload = {
|
||
"users" => {
|
||
first_post.user_id.to_s => {
|
||
"id" => first_post.user.id,
|
||
"badge_ids" => [badge.id],
|
||
},
|
||
},
|
||
"badges" => {
|
||
badge.id.to_s => {
|
||
"id" => badge.id,
|
||
"name" => badge.name,
|
||
"slug" => badge.slug,
|
||
"description" => badge.description,
|
||
"icon" => badge.icon,
|
||
"image_url" => badge.image_url,
|
||
"badge_grouping_id" => badge.badge_grouping_id,
|
||
"badge_type_id" => badge.badge_type_id,
|
||
},
|
||
},
|
||
}
|
||
|
||
get "/t/#{topic.slug}/#{topic.id}.json"
|
||
user_badges = response.parsed_body["user_badges"]
|
||
expect(user_badges).to eq(expected_payload)
|
||
|
||
get "/t/#{topic.id}/posts.json?post_ids[]=#{first_post.id}"
|
||
user_badges = response.parsed_body["user_badges"]
|
||
expect(user_badges).to eq(expected_payload)
|
||
end
|
||
end
|
||
|
||
context "with registered redirect_to_correct_topic_additional_query_parameters" do
|
||
let(:modifier_block) { Proc.new { |allowed_params| allowed_params << :silly_param } }
|
||
|
||
it "retains the permitted query param when redirecting" do
|
||
plugin_instance = Plugin::Instance.new
|
||
plugin_instance.register_modifier(
|
||
:redirect_to_correct_topic_additional_query_parameters,
|
||
&modifier_block
|
||
)
|
||
|
||
get "/t/#{topic.slug}", params: { silly_param: "hehe" }
|
||
|
||
expect(response).to redirect_to("#{topic.relative_url}?silly_param=hehe")
|
||
ensure
|
||
DiscoursePluginRegistry.unregister_modifier(
|
||
plugin_instance,
|
||
:redirect_to_correct_topic_additional_query_parameters,
|
||
&modifier_block
|
||
)
|
||
end
|
||
end
|
||
|
||
context "when a topic with nil slug exists" do
|
||
before do
|
||
nil_slug_topic = Fabricate(:topic)
|
||
Topic.connection.execute("update topics set slug=null where id = #{nil_slug_topic.id}") # can't find a way to set slug column to null using the model
|
||
end
|
||
|
||
it "returns a 404 when slug and topic id do not match a topic" do
|
||
get "/t/made-up-topic-slug/123123.json"
|
||
expect(response.status).to eq(404)
|
||
end
|
||
end
|
||
|
||
context "with permission errors" do
|
||
fab!(:allowed_user, :user)
|
||
fab!(:allowed_group, :group)
|
||
fab!(:accessible_group) { Fabricate(:group, public_admission: true) }
|
||
fab!(:secure_category) do
|
||
c = Fabricate(:category)
|
||
c.permissions = [[allowed_group, :full]]
|
||
c.save
|
||
allowed_user.groups = [allowed_group]
|
||
allowed_user.save
|
||
c
|
||
end
|
||
fab!(:accessible_category) do
|
||
Fabricate(:category).tap do |c|
|
||
c.set_permissions(accessible_group => :full)
|
||
c.save!
|
||
end
|
||
end
|
||
fab!(:normal_topic, :topic)
|
||
fab!(:secure_topic) { Fabricate(:topic, category: secure_category) }
|
||
fab!(:private_topic) { Fabricate(:private_message_topic, user: allowed_user) }
|
||
|
||
# Can't use fab!, because deleted_topics can't be re-found
|
||
before_all do
|
||
@deleted_topic = Fabricate(:deleted_topic)
|
||
@deleted_secure_topic = Fabricate(:topic, category: secure_category, deleted_at: 1.day.ago)
|
||
@deleted_private_topic =
|
||
Fabricate(:private_message_topic, user: allowed_user, deleted_at: 1.day.ago)
|
||
end
|
||
let(:deleted_topic) { @deleted_topic }
|
||
let(:deleted_secure_topic) { @deleted_secure_topic }
|
||
let(:deleted_private_topic) { @deleted_private_topic }
|
||
|
||
let!(:nonexistent_topic_id) { Topic.last.id + 10_000 }
|
||
fab!(:secure_accessible_topic) { Fabricate(:topic, category: accessible_category) }
|
||
|
||
shared_examples "various scenarios" do |expected, request_json:|
|
||
expected.each do |key, value|
|
||
it "returns #{value} for #{key}" do
|
||
slug = key == :nonexistent ? "garbage-slug" : send(key.to_s).slug
|
||
topic_id = key == :nonexistent ? nonexistent_topic_id : send(key.to_s).id
|
||
format = request_json ? ".json" : ""
|
||
get "/t/#{slug}/#{topic_id}#{format}"
|
||
expect(response.status).to eq(value)
|
||
end
|
||
end
|
||
|
||
expected_slug_response = expected[:secure_topic] == 200 ? 301 : expected[:secure_topic]
|
||
it "will return a #{expected_slug_response} when requesting a secure topic by slug" do
|
||
format = request_json ? ".json" : ""
|
||
get "/t/#{secure_topic.slug}#{format}"
|
||
expect(response.status).to eq(expected_slug_response)
|
||
end
|
||
end
|
||
|
||
context "without detailed error pages" do
|
||
before { SiteSetting.detailed_404 = false }
|
||
|
||
context "when anonymous" do
|
||
expected = {
|
||
normal_topic: 200,
|
||
secure_topic: 404,
|
||
private_topic: 404,
|
||
deleted_topic: 404,
|
||
deleted_secure_topic: 404,
|
||
deleted_private_topic: 404,
|
||
nonexistent: 404,
|
||
secure_accessible_topic: 404,
|
||
}
|
||
include_examples "various scenarios", expected, request_json: false
|
||
end
|
||
|
||
context "when anonymous with login required" do
|
||
before { SiteSetting.login_required = true }
|
||
expected = {
|
||
normal_topic: 302,
|
||
secure_topic: 302,
|
||
private_topic: 302,
|
||
deleted_topic: 302,
|
||
deleted_secure_topic: 302,
|
||
deleted_private_topic: 302,
|
||
nonexistent: 302,
|
||
secure_accessible_topic: 302,
|
||
}
|
||
include_examples "various scenarios", expected, request_json: false
|
||
end
|
||
|
||
context "when anonymous with login required, requesting json" do
|
||
before { SiteSetting.login_required = true }
|
||
expected = {
|
||
normal_topic: 403,
|
||
secure_topic: 403,
|
||
private_topic: 403,
|
||
deleted_topic: 403,
|
||
deleted_secure_topic: 403,
|
||
deleted_private_topic: 403,
|
||
nonexistent: 403,
|
||
secure_accessible_topic: 403,
|
||
}
|
||
include_examples "various scenarios", expected, request_json: true
|
||
end
|
||
|
||
context "when normal user" do
|
||
before { sign_in(user) }
|
||
|
||
expected = {
|
||
normal_topic: 200,
|
||
secure_topic: 404,
|
||
private_topic: 404,
|
||
deleted_topic: 404,
|
||
deleted_secure_topic: 404,
|
||
deleted_private_topic: 404,
|
||
nonexistent: 404,
|
||
secure_accessible_topic: 404,
|
||
}
|
||
include_examples "various scenarios", expected, request_json: false
|
||
end
|
||
|
||
context "when allowed user" do
|
||
before { sign_in(allowed_user) }
|
||
|
||
expected = {
|
||
normal_topic: 200,
|
||
secure_topic: 200,
|
||
private_topic: 200,
|
||
deleted_topic: 404,
|
||
deleted_secure_topic: 404,
|
||
deleted_private_topic: 404,
|
||
nonexistent: 404,
|
||
secure_accessible_topic: 404,
|
||
}
|
||
include_examples "various scenarios", expected, request_json: false
|
||
end
|
||
|
||
context "when moderator" do
|
||
before { sign_in(moderator) }
|
||
|
||
expected = {
|
||
normal_topic: 200,
|
||
secure_topic: 404,
|
||
private_topic: 404,
|
||
deleted_topic: 200,
|
||
deleted_secure_topic: 404,
|
||
deleted_private_topic: 404,
|
||
nonexistent: 404,
|
||
secure_accessible_topic: 404,
|
||
}
|
||
include_examples "various scenarios", expected, request_json: false
|
||
end
|
||
|
||
context "when admin" do
|
||
before { sign_in(admin) }
|
||
|
||
expected = {
|
||
normal_topic: 200,
|
||
secure_topic: 200,
|
||
private_topic: 200,
|
||
deleted_topic: 200,
|
||
deleted_secure_topic: 200,
|
||
deleted_private_topic: 200,
|
||
nonexistent: 404,
|
||
secure_accessible_topic: 200,
|
||
}
|
||
include_examples "various scenarios", expected, request_json: false
|
||
end
|
||
end
|
||
|
||
context "with detailed error pages" do
|
||
before { SiteSetting.detailed_404 = true }
|
||
|
||
context "when anonymous" do
|
||
expected = {
|
||
normal_topic: 200,
|
||
secure_topic: 403,
|
||
private_topic: 403,
|
||
deleted_topic: 410,
|
||
deleted_secure_topic: 403,
|
||
deleted_private_topic: 403,
|
||
nonexistent: 404,
|
||
secure_accessible_topic: 403,
|
||
}
|
||
include_examples "various scenarios", expected, request_json: true
|
||
end
|
||
|
||
context "when anonymous with login required" do
|
||
before { SiteSetting.login_required = true }
|
||
expected = {
|
||
normal_topic: 302,
|
||
secure_topic: 302,
|
||
private_topic: 302,
|
||
deleted_topic: 302,
|
||
deleted_secure_topic: 302,
|
||
deleted_private_topic: 302,
|
||
nonexistent: 302,
|
||
secure_accessible_topic: 302,
|
||
}
|
||
include_examples "various scenarios", expected, request_json: false
|
||
end
|
||
|
||
context "when normal user" do
|
||
before { sign_in(user) }
|
||
|
||
expected = {
|
||
normal_topic: 200,
|
||
secure_topic: 403,
|
||
private_topic: 403,
|
||
deleted_topic: 410,
|
||
deleted_secure_topic: 403,
|
||
deleted_private_topic: 403,
|
||
nonexistent: 404,
|
||
secure_accessible_topic: 403,
|
||
}
|
||
include_examples "various scenarios", expected, request_json: true
|
||
end
|
||
|
||
context "when allowed user" do
|
||
before { sign_in(allowed_user) }
|
||
|
||
expected = {
|
||
normal_topic: 200,
|
||
secure_topic: 200,
|
||
private_topic: 200,
|
||
deleted_topic: 410,
|
||
deleted_secure_topic: 410,
|
||
deleted_private_topic: 410,
|
||
nonexistent: 404,
|
||
secure_accessible_topic: 403,
|
||
}
|
||
include_examples "various scenarios", expected, request_json: false
|
||
end
|
||
|
||
context "when moderator" do
|
||
before { sign_in(moderator) }
|
||
|
||
expected = {
|
||
normal_topic: 200,
|
||
secure_topic: 403,
|
||
private_topic: 403,
|
||
deleted_topic: 200,
|
||
deleted_secure_topic: 403,
|
||
deleted_private_topic: 403,
|
||
nonexistent: 404,
|
||
secure_accessible_topic: 403,
|
||
}
|
||
include_examples "various scenarios", expected, request_json: false
|
||
end
|
||
|
||
context "when admin" do
|
||
before { sign_in(admin) }
|
||
|
||
expected = {
|
||
normal_topic: 200,
|
||
secure_topic: 200,
|
||
private_topic: 200,
|
||
deleted_topic: 200,
|
||
deleted_secure_topic: 200,
|
||
deleted_private_topic: 200,
|
||
nonexistent: 404,
|
||
secure_accessible_topic: 200,
|
||
}
|
||
include_examples "various scenarios", expected, request_json: false
|
||
end
|
||
end
|
||
end
|
||
|
||
it "does not record a topic view" do
|
||
expect { get "/t/#{topic.slug}/#{topic.id}.json" }.not_to change(TopicViewItem, :count)
|
||
end
|
||
|
||
it "records a view to invalid post_number" do
|
||
expect do
|
||
get "/t/#{topic.slug}/#{topic.id}/#{256**4}", params: { u: user.username }
|
||
expect(response.status).to eq(200)
|
||
end.to change { IncomingLink.count }.by(1)
|
||
end
|
||
|
||
it "records incoming links" do
|
||
expect do get "/t/#{topic.slug}/#{topic.id}", params: { u: user.username } end.to change {
|
||
IncomingLink.count
|
||
}.by(1)
|
||
end
|
||
|
||
context "with print" do
|
||
it "doesn't renders the print view when disabled" do
|
||
SiteSetting.max_prints_per_hour_per_user = 0
|
||
|
||
get "/t/#{topic.slug}/#{topic.id}/print"
|
||
|
||
expect(response).to be_forbidden
|
||
end
|
||
|
||
it "renders the print view when enabled" do
|
||
SiteSetting.max_prints_per_hour_per_user = 10
|
||
get "/t/#{topic.slug}/#{topic.id}/print", headers: { HTTP_USER_AGENT: "Rails Testing" }
|
||
|
||
expect(response.status).to eq(200)
|
||
body = response.body
|
||
|
||
expect(body).to have_tag(:body, class: "crawler")
|
||
expect(body).to_not have_tag(:meta, with: { name: "fragment" })
|
||
end
|
||
|
||
it "uses the application layout when there's no param" do
|
||
SiteSetting.max_prints_per_hour_per_user = 10
|
||
get "/t/#{topic.slug}/#{topic.id}", headers: { HTTP_USER_AGENT: "Rails Testing" }
|
||
|
||
body = response.body
|
||
|
||
expect(body).to have_tag(:script, with: { "data-discourse-entrypoint" => "discourse" })
|
||
expect(body).to have_tag(:meta, with: { name: "fragment" })
|
||
end
|
||
|
||
context "with restricted tags" do
|
||
let(:tag_group) { Fabricate.build(:tag_group) }
|
||
let(:tag_group_permission) { Fabricate.build(:tag_group_permission, tag_group: tag_group) }
|
||
let(:restricted_tag) { Fabricate(:tag) }
|
||
let(:public_tag) { Fabricate(:tag) }
|
||
|
||
before do
|
||
# avoid triggering a `before_create` callback in `TagGroup` which
|
||
# messes with permissions
|
||
tag_group.tag_group_permissions << tag_group_permission
|
||
tag_group.save!
|
||
tag_group_permission.tag_group.tags << restricted_tag
|
||
topic.tags << [public_tag, restricted_tag]
|
||
end
|
||
|
||
it "doesn’t expose restricted tags" do
|
||
get "/t/#{topic.slug}/#{topic.id}/print", headers: { HTTP_USER_AGENT: "Rails Testing" }
|
||
expect(response.body).to match(public_tag.name)
|
||
expect(response.body).not_to match(restricted_tag.name)
|
||
end
|
||
end
|
||
end
|
||
|
||
it "records the referer for a visit arriving via redirect" do
|
||
get "/t/#{topic.id}", headers: { HTTP_REFERER: "http://twitter.com" }
|
||
# Simulate browsers, which preserve Referer across same-origin redirects
|
||
follow_redirect!(headers: { "HTTP_REFERER" => "http://twitter.com" })
|
||
|
||
link = IncomingLink.first
|
||
expect(link.referer).to eq("http://twitter.com")
|
||
end
|
||
|
||
it "tracks a visit for all html requests" do
|
||
sign_in(user)
|
||
get "/t/#{topic.slug}/#{topic.id}"
|
||
topic_user = TopicUser.where(user: user, topic: topic).first
|
||
expect(topic_user.last_visited_at).to eq_time(topic_user.first_visited_at)
|
||
end
|
||
|
||
context "when considering for a promotion" do
|
||
before do
|
||
SiteSetting.tl1_requires_topics_entered = 0
|
||
SiteSetting.tl1_requires_read_posts = 0
|
||
SiteSetting.tl1_requires_time_spent_mins = 0
|
||
SiteSetting.tl1_requires_time_spent_mins = 0
|
||
end
|
||
|
||
it "reviews the user for a promotion if they're new" do
|
||
sign_in(user)
|
||
user.update_column(:trust_level, TrustLevel[0])
|
||
get "/t/#{topic.slug}/#{topic.id}.json"
|
||
user.reload
|
||
expect(user.trust_level).to eq(1)
|
||
end
|
||
end
|
||
|
||
context "with filters" do
|
||
def extract_post_stream
|
||
json = response.parsed_body
|
||
json["post_stream"]["posts"].map { |post| post["id"] }
|
||
end
|
||
|
||
before do
|
||
TopicView.stubs(:chunk_size).returns(2)
|
||
@post_ids = topic.posts.pluck(:id)
|
||
3.times { @post_ids << Fabricate(:post, user: post_author1, topic: topic).id }
|
||
end
|
||
|
||
it "grabs the correct set of posts" do
|
||
get "/t/#{topic.slug}/#{topic.id}.json"
|
||
expect(response.status).to eq(200)
|
||
expect(extract_post_stream).to eq(@post_ids[0..1])
|
||
|
||
get "/t/#{topic.slug}/#{topic.id}.json", params: { page: 1 }
|
||
expect(response.status).to eq(200)
|
||
expect(extract_post_stream).to eq(@post_ids[0..1])
|
||
|
||
get "/t/#{topic.slug}/#{topic.id}.json", params: { page: 2 }
|
||
expect(response.status).to eq(200)
|
||
expect(extract_post_stream).to eq(@post_ids[2..3])
|
||
|
||
post_number = topic.posts.pluck(:post_number).sort[3]
|
||
get "/t/#{topic.slug}/#{topic.id}/#{post_number}.json"
|
||
expect(response.status).to eq(200)
|
||
expect(extract_post_stream).to eq(@post_ids[-2..-1])
|
||
|
||
TopicView.stubs(:chunk_size).returns(3)
|
||
|
||
get "/t/#{topic.slug}/#{topic.id}.json", params: { page: 1 }
|
||
expect(response.status).to eq(200)
|
||
expect(extract_post_stream).to eq(@post_ids[0..2])
|
||
|
||
get "/t/#{topic.slug}/#{topic.id}.json", params: { page: 2 }
|
||
expect(response.status).to eq(200)
|
||
expect(extract_post_stream).to eq(@post_ids[3..3])
|
||
|
||
get "/t/#{topic.slug}/#{topic.id}.json", params: { page: 3 }
|
||
expect(response).to redirect_to("/t/#{topic.slug}/#{topic.id}.json?page=2")
|
||
|
||
TopicView.stubs(:chunk_size).returns(4)
|
||
|
||
get "/t/#{topic.slug}/#{topic.id}.json", params: { page: 1 }
|
||
expect(response.status).to eq(200)
|
||
expect(extract_post_stream).to eq(@post_ids[0..3])
|
||
|
||
get "/t/#{topic.slug}/#{topic.id}.json", params: { page: 2 }
|
||
expect(response).to redirect_to("/t/#{topic.slug}/#{topic.id}.json")
|
||
end
|
||
end
|
||
|
||
describe "with external permalink on a soft-deleted topic" do
|
||
fab!(:topic) { Fabricate(:post, user: post_author1).topic }
|
||
|
||
before do
|
||
topic.trash!(Discourse.system_user)
|
||
Permalink.create!(
|
||
url: "t/#{topic.slug}/#{topic.id}",
|
||
external_url: "https://www.example.com",
|
||
)
|
||
end
|
||
|
||
it "returns Discourse-Xhr-Redirect header for XHR requests" do
|
||
get "/t/#{topic.slug}/#{topic.id}.json",
|
||
headers: {
|
||
"HTTP_X_REQUESTED_WITH" => "XMLHttpRequest",
|
||
}
|
||
|
||
expect(response.status).to eq(200)
|
||
expect(response.headers["Discourse-Xhr-Redirect"]).to eq("true")
|
||
expect(response.body).to eq("https://www.example.com")
|
||
end
|
||
|
||
it "returns Discourse-Xhr-Redirect header for XHR requests when detailed_404 is enabled" do
|
||
SiteSetting.detailed_404 = true
|
||
|
||
get "/t/#{topic.slug}/#{topic.id}.json",
|
||
headers: {
|
||
"HTTP_X_REQUESTED_WITH" => "XMLHttpRequest",
|
||
}
|
||
|
||
expect(response.status).to eq(200)
|
||
expect(response.headers["Discourse-Xhr-Redirect"]).to eq("true")
|
||
expect(response.body).to eq("https://www.example.com")
|
||
end
|
||
|
||
it "returns 301 redirect for non-XHR requests" do
|
||
get "/t/#{topic.slug}/#{topic.id}"
|
||
|
||
expect(response.status).to eq(301)
|
||
expect(response.headers["Location"]).to eq("https://www.example.com")
|
||
end
|
||
end
|
||
|
||
describe "with external permalink on a nonexistent topic" do
|
||
let!(:nonexistent_topic_id) { Topic.maximum(:id) + 1 }
|
||
|
||
before do
|
||
Permalink.create!(
|
||
url: "t/old-topic/#{nonexistent_topic_id}",
|
||
external_url: "https://www.example.com",
|
||
)
|
||
end
|
||
|
||
it "returns Discourse-Xhr-Redirect header for XHR requests" do
|
||
get "/t/old-topic/#{nonexistent_topic_id}.json",
|
||
headers: {
|
||
"HTTP_X_REQUESTED_WITH" => "XMLHttpRequest",
|
||
}
|
||
|
||
expect(response.status).to eq(200)
|
||
expect(response.headers["Discourse-Xhr-Redirect"]).to eq("true")
|
||
expect(response.body).to eq("https://www.example.com")
|
||
end
|
||
|
||
it "returns 301 redirect for non-XHR requests" do
|
||
get "/t/old-topic/#{nonexistent_topic_id}"
|
||
|
||
expect(response.status).to eq(301)
|
||
expect(response.headers["Location"]).to eq("https://www.example.com")
|
||
end
|
||
end
|
||
|
||
describe "#show filters" do
|
||
fab!(:post) { Fabricate(:post, user: post_author1) }
|
||
fab!(:topic) { post.topic }
|
||
fab!(:post2) { Fabricate(:post, user: post_author2, topic: topic) }
|
||
|
||
describe "filter by replies to a post" do
|
||
fab!(:post3) do
|
||
Fabricate(
|
||
:post,
|
||
user: post_author3,
|
||
topic: topic,
|
||
reply_to_post_number: post2.post_number,
|
||
)
|
||
end
|
||
fab!(:post4) do
|
||
Fabricate(
|
||
:post,
|
||
user: post_author4,
|
||
topic: topic,
|
||
reply_to_post_number: post2.post_number,
|
||
)
|
||
end
|
||
fab!(:post5) { Fabricate(:post, user: post_author5, topic: topic) }
|
||
fab!(:quote_reply) { Fabricate(:basic_reply, user: user, topic: topic) }
|
||
fab!(:post_reply) { PostReply.create(post_id: post2.id, reply_post_id: quote_reply.id) }
|
||
|
||
it "should return the right posts" do
|
||
get "/t/#{topic.id}.json", params: { replies_to_post_number: post2.post_number }
|
||
|
||
expect(response.status).to eq(200)
|
||
|
||
body = response.parsed_body
|
||
|
||
expect(body.has_key?("suggested_topics")).to eq(false)
|
||
expect(body.has_key?("related_messages")).to eq(false)
|
||
|
||
ids = body["post_stream"]["posts"].map { |p| p["id"] }
|
||
expect(ids).to eq([post.id, post2.id, post3.id, post4.id, quote_reply.id])
|
||
end
|
||
end
|
||
|
||
describe "filter by top level replies" do
|
||
fab!(:post3) do
|
||
Fabricate(
|
||
:post,
|
||
user: post_author3,
|
||
topic: topic,
|
||
reply_to_post_number: post2.post_number,
|
||
)
|
||
end
|
||
fab!(:post4) do
|
||
Fabricate(
|
||
:post,
|
||
user: post_author4,
|
||
topic: topic,
|
||
reply_to_post_number: post2.post_number,
|
||
)
|
||
end
|
||
fab!(:post5) { Fabricate(:post, user: post_author5, topic: topic) }
|
||
fab!(:post6) do
|
||
Fabricate(
|
||
:post,
|
||
user: post_author4,
|
||
topic: topic,
|
||
reply_to_post_number: post5.post_number,
|
||
)
|
||
end
|
||
|
||
it "should return the right posts" do
|
||
get "/t/#{topic.id}.json", params: { filter_top_level_replies: true }
|
||
|
||
expect(response.status).to eq(200)
|
||
|
||
body = response.parsed_body
|
||
|
||
expect(body.has_key?("suggested_topics")).to eq(false)
|
||
expect(body.has_key?("related_messages")).to eq(false)
|
||
|
||
ids = body["post_stream"]["posts"].map { |p| p["id"] }
|
||
expect(ids).to eq([post2.id, post5.id])
|
||
end
|
||
end
|
||
|
||
describe "filter upwards by post id" do
|
||
fab!(:post3) { Fabricate(:post, user: post_author3, topic: topic) }
|
||
fab!(:post4) do
|
||
Fabricate(
|
||
:post,
|
||
user: post_author4,
|
||
topic: topic,
|
||
reply_to_post_number: post3.post_number,
|
||
)
|
||
end
|
||
fab!(:post5) do
|
||
Fabricate(
|
||
:post,
|
||
user: post_author5,
|
||
topic: topic,
|
||
reply_to_post_number: post4.post_number,
|
||
)
|
||
end
|
||
fab!(:post6) { Fabricate(:post, user: post_author6, topic: topic) }
|
||
|
||
it "should return the right posts" do
|
||
get "/t/#{topic.id}.json", params: { filter_upwards_post_id: post5.id }
|
||
|
||
expect(response.status).to eq(200)
|
||
|
||
body = response.parsed_body
|
||
|
||
expect(body.has_key?("suggested_topics")).to eq(false)
|
||
expect(body.has_key?("related_messages")).to eq(false)
|
||
|
||
ids = body["post_stream"]["posts"].map { |p| p["id"] }
|
||
# includes topic OP, current post and subsequent posts
|
||
# but only one level of parents, respecting default max_reply_history = 1
|
||
expect(ids).to eq([post.id, post4.id, post5.id, post6.id])
|
||
end
|
||
|
||
it "should respect max_reply_history site setting" do
|
||
SiteSetting.max_reply_history = 2
|
||
|
||
get "/t/#{topic.id}.json", params: { filter_upwards_post_id: post5.id }
|
||
|
||
expect(response.status).to eq(200)
|
||
|
||
body = response.parsed_body
|
||
ids = body["post_stream"]["posts"].map { |p| p["id"] }
|
||
|
||
# includes 2 levels of replies (post3 and post4)
|
||
expect(ids).to eq([post.id, post3.id, post4.id, post5.id, post6.id])
|
||
end
|
||
end
|
||
end
|
||
|
||
context "when 'login required' site setting has been enabled" do
|
||
before { SiteSetting.login_required = true }
|
||
|
||
context "when the user is logged in" do
|
||
before { sign_in(user) }
|
||
|
||
it "shows the topic" do
|
||
get "/t/#{topic.slug}/#{topic.id}.json"
|
||
expect(response.status).to eq(200)
|
||
end
|
||
end
|
||
|
||
context "when the user is not logged in" do
|
||
let(:api_key) { Fabricate(:api_key, user: topic.user) }
|
||
|
||
it "redirects browsers to the login page" do
|
||
get "/t/#{topic.slug}/#{topic.id}"
|
||
expect(response).to redirect_to login_path
|
||
end
|
||
|
||
it "raises a 403 for json requests" do
|
||
get "/t/#{topic.slug}/#{topic.id}.json"
|
||
expect(response.status).to eq(403)
|
||
end
|
||
|
||
it "shows the topic if valid api key is provided" do
|
||
get "/t/#{topic.slug}/#{topic.id}.json", headers: { "HTTP_API_KEY" => api_key.key }
|
||
|
||
expect(response.status).to eq(200)
|
||
topic.reload
|
||
end
|
||
|
||
it "returns 403 for an invalid key" do
|
||
%i[json html].each do |format|
|
||
get "/t/#{topic.slug}/#{topic.id}.#{format}", headers: { "HTTP_API_KEY" => "bad" }
|
||
|
||
expect(response.code.to_i).to eq(403)
|
||
expect(response.body).to include(I18n.t("invalid_access"))
|
||
end
|
||
end
|
||
end
|
||
end
|
||
|
||
it "is included for unlisted topics" do
|
||
get "/t/#{invisible_topic.slug}/#{invisible_topic.id}.json"
|
||
|
||
expect(response.headers["X-Robots-Tag"]).to eq("noindex")
|
||
end
|
||
|
||
it "is not included for normal topics" do
|
||
get "/t/#{topic.slug}/#{topic.id}.json"
|
||
|
||
expect(response.headers["X-Robots-Tag"]).to eq(nil)
|
||
end
|
||
|
||
it "is included when allow_index_in_robots_txt is set to false" do
|
||
SiteSetting.allow_index_in_robots_txt = false
|
||
|
||
get "/t/#{topic.slug}/#{topic.id}.json"
|
||
|
||
expect(response.headers["X-Robots-Tag"]).to eq("noindex, nofollow")
|
||
end
|
||
|
||
it "doesn't store an incoming link when there's no referer" do
|
||
expect { get "/t/#{topic.id}.json" }.not_to change(IncomingLink, :count)
|
||
expect(response.status).to eq(200)
|
||
end
|
||
|
||
it "doesn't raise an error on a very long link" do
|
||
get "/t/#{topic.id}.json", headers: { HTTP_REFERER: "http://#{"a" * 2000}.com" }
|
||
expect(response.status).to eq(200)
|
||
end
|
||
|
||
context "when `enable_user_status` site setting is enabled" do
|
||
fab!(:post) { Fabricate(:post, user: post_author1) }
|
||
fab!(:topic) { post.topic }
|
||
fab!(:post2) do
|
||
Fabricate(
|
||
:post,
|
||
user: post_author2,
|
||
topic: topic,
|
||
raw: "I am mentioning @#{post_author1.username}.",
|
||
)
|
||
end
|
||
|
||
before { SiteSetting.enable_user_status = true }
|
||
|
||
it "does not return mentions when `enable_user_status` site setting is disabled" do
|
||
SiteSetting.enable_user_status = false
|
||
|
||
get "/t/#{topic.slug}/#{topic.id}.json"
|
||
|
||
expect(response.status).to eq(200)
|
||
|
||
json = response.parsed_body
|
||
|
||
expect(json["post_stream"]["posts"][1]["mentioned_users"]).to eq(nil)
|
||
end
|
||
|
||
it "returns mentions with status" do
|
||
post_author1.set_status!("off to dentist", "tooth")
|
||
|
||
get "/t/#{topic.slug}/#{topic.id}.json"
|
||
|
||
expect(response.status).to eq(200)
|
||
|
||
json = response.parsed_body
|
||
expect(json["post_stream"]["posts"][1]["mentioned_users"].length).to be(1)
|
||
|
||
mentioned_user = json["post_stream"]["posts"][1]["mentioned_users"][0]
|
||
expect(mentioned_user["id"]).to be(post_author1.id)
|
||
expect(mentioned_user["name"]).to eq(post_author1.name)
|
||
expect(mentioned_user["username"]).to eq(post_author1.username)
|
||
|
||
status = mentioned_user["status"]
|
||
expect(status).to be_present
|
||
expect(status["emoji"]).to eq(post_author1.user_status.emoji)
|
||
expect(status["description"]).to eq(post_author1.user_status.description)
|
||
end
|
||
|
||
it "returns an empty list of mentioned users if there are no mentions in a post" do
|
||
Fabricate(:post, user: post_author2, topic: topic, raw: "Post without mentions.")
|
||
|
||
get "/t/#{topic.slug}/#{topic.id}.json"
|
||
|
||
expect(response.status).to eq(200)
|
||
|
||
json = response.parsed_body
|
||
expect(json["post_stream"]["posts"][2]["mentioned_users"].length).to be(0)
|
||
end
|
||
|
||
it "returns an empty list of mentioned users if an unexisting user was mentioned" do
|
||
Fabricate(:post, user: post_author2, topic: topic, raw: "Mentioning an @unexisting_user.")
|
||
|
||
get "/t/#{topic.slug}/#{topic.id}.json"
|
||
|
||
expect(response.status).to eq(200)
|
||
|
||
json = response.parsed_body
|
||
expect(json["post_stream"]["posts"][2]["mentioned_users"].length).to be(0)
|
||
end
|
||
end
|
||
|
||
describe "has_escaped_fragment?" do
|
||
context "when the SiteSetting is disabled" do
|
||
it "uses the application layout even with an escaped fragment param" do
|
||
SiteSetting.enable_escaped_fragments = false
|
||
|
||
get "/t/#{topic.slug}/#{topic.id}", params: { _escaped_fragment_: "true" }
|
||
|
||
body = response.body
|
||
|
||
expect(response.status).to eq(200)
|
||
expect(body).to have_tag(:script, with: { "data-discourse-entrypoint" => "discourse" })
|
||
expect(body).to_not have_tag(:meta, with: { name: "fragment" })
|
||
end
|
||
end
|
||
|
||
context "when the SiteSetting is enabled" do
|
||
before { SiteSetting.enable_escaped_fragments = true }
|
||
|
||
it "uses the application layout when there's no param" do
|
||
get "/t/#{topic.slug}/#{topic.id}"
|
||
|
||
body = response.body
|
||
|
||
expect(body).to have_tag(:script, with: { "data-discourse-entrypoint" => "discourse" })
|
||
expect(body).to have_tag(:meta, with: { name: "fragment" })
|
||
end
|
||
|
||
it "uses the crawler layout when there's an _escaped_fragment_ param" do
|
||
get "/t/#{topic.slug}/#{topic.id}",
|
||
params: {
|
||
_escaped_fragment_: true,
|
||
},
|
||
headers: {
|
||
HTTP_USER_AGENT: "Rails Testing",
|
||
}
|
||
|
||
body = response.body
|
||
|
||
expect(response.status).to eq(200)
|
||
expect(body).to have_tag(:body, with: { class: "crawler" })
|
||
expect(body).to_not have_tag(:meta, with: { name: "fragment" })
|
||
end
|
||
end
|
||
end
|
||
|
||
describe "clear_notifications" do
|
||
it "correctly clears notifications if specified via cookie" do
|
||
set_subfolder "/eviltrout"
|
||
|
||
notification = Fabricate(:notification)
|
||
sign_in(notification.user)
|
||
|
||
cookies["cn"] = "2828,100,#{notification.id}"
|
||
|
||
get "/t/#{topic.id}.json"
|
||
|
||
expect(response.status).to eq(200)
|
||
expect(response.cookies["cn"]).to eq(nil)
|
||
expect(response.headers["Set-Cookie"]).to match(%r{^cn=;.*path=/eviltrout})
|
||
|
||
notification.reload
|
||
expect(notification.read).to eq(true)
|
||
end
|
||
|
||
it "correctly clears notifications if specified via header" do
|
||
notification = Fabricate(:notification)
|
||
sign_in(notification.user)
|
||
|
||
get "/t/#{topic.id}.json",
|
||
headers: {
|
||
"Discourse-Clear-Notifications" => "2828,100,#{notification.id}",
|
||
}
|
||
|
||
expect(response.status).to eq(200)
|
||
notification.reload
|
||
expect(notification.read).to eq(true)
|
||
end
|
||
end
|
||
|
||
describe "read only header" do
|
||
it "returns no read only header by default" do
|
||
get "/t/#{topic.id}.json"
|
||
expect(response.status).to eq(200)
|
||
expect(response.headers["Discourse-Readonly"]).to eq(nil)
|
||
end
|
||
|
||
it "returns a readonly header if the site is read only" do
|
||
Discourse.received_postgres_readonly!
|
||
get "/t/#{topic.id}.json"
|
||
expect(response.status).to eq(200)
|
||
expect(response.headers["Discourse-Readonly"]).to eq("true")
|
||
end
|
||
end
|
||
|
||
describe "image only topic" do
|
||
it "uses image alt tag for meta description" do
|
||
post =
|
||
Fabricate(
|
||
:post,
|
||
user: post_author1,
|
||
raw: "",
|
||
)
|
||
|
||
get post.topic.url
|
||
|
||
body = response.body
|
||
expect(body).to have_tag(
|
||
:meta,
|
||
with: {
|
||
name: "description",
|
||
content: "[image_description]",
|
||
},
|
||
)
|
||
end
|
||
|
||
it "uses image cdn url for schema markup" do
|
||
set_cdn_url("http://cdn.localhost")
|
||
post = Fabricate(:post_with_uploaded_image, user: post_author1)
|
||
CookedPostProcessor.new(post).update_post_image
|
||
|
||
get post.topic.url
|
||
|
||
body = response.body
|
||
expect(body).to have_tag(:link, with: { itemprop: "image", href: post.image_url })
|
||
end
|
||
end
|
||
|
||
it "returns suggested topics only when loading the last chunk of posts in a topic" do
|
||
topic_post_2 = Fabricate(:post, topic: topic)
|
||
topic_post_3 = Fabricate(:post, topic: topic)
|
||
topic_post_4 = Fabricate(:post, topic: topic)
|
||
|
||
stub_const(TopicView, "CHUNK_SIZE", 2) do
|
||
get "/t/#{topic.slug}/#{topic.id}.json"
|
||
|
||
expect(response.status).to eq(200)
|
||
expect(response.parsed_body.has_key?("suggested_topics")).to eq(false)
|
||
|
||
get "/t/#{topic.slug}/#{topic.id}/4.json"
|
||
|
||
expect(response.status).to eq(200)
|
||
expect(response.parsed_body.has_key?("suggested_topics")).to eq(true)
|
||
end
|
||
end
|
||
|
||
it "returns a list of categories when `lazy_load_categories_group` site setting is enabled for the current user" do
|
||
SiteSetting.lazy_load_categories_groups = "#{Group::AUTO_GROUPS[:anonymous_users]}"
|
||
|
||
topic_post_2 = Fabricate(:post, topic: topic)
|
||
topic_post_3 = Fabricate(:post, topic: topic)
|
||
topic_post_4 = Fabricate(:post, topic: topic)
|
||
dest_topic.update!(category: Fabricate(:category))
|
||
|
||
stub_const(TopicView, "CHUNK_SIZE", 2) do
|
||
get "/t/#{topic.slug}/#{topic.id}.json"
|
||
|
||
expect(response.status).to eq(200)
|
||
expect(response.parsed_body.has_key?("suggested_topics")).to eq(false)
|
||
expect(response.parsed_body["categories"].map { it["id"] }).to contain_exactly(
|
||
topic.category_id,
|
||
)
|
||
|
||
get "/t/#{topic.slug}/#{topic.id}/4.json"
|
||
|
||
expect(response.status).to eq(200)
|
||
expect(response.parsed_body.has_key?("suggested_topics")).to eq(true)
|
||
expect(response.parsed_body["categories"].map { it["id"] }).to contain_exactly(
|
||
topic.category_id,
|
||
dest_topic.category_id,
|
||
)
|
||
end
|
||
end
|
||
end
|
||
|
||
describe "topic access errors with detailed_404 disabled" do
|
||
it "returns not found for inaccessible and nonexistent topics on secondary topic endpoints" do
|
||
SiteSetting.detailed_404 = false
|
||
sign_in(user)
|
||
|
||
private_message =
|
||
create_post(
|
||
user: admin,
|
||
archetype: Archetype.private_message,
|
||
target_usernames: [moderator.username],
|
||
).topic
|
||
nonexistent_topic_id = Topic.maximum(:id) + 10_000
|
||
requests = {
|
||
"GET /t/:topic_id/wordpress.json" => ->(topic_id) do
|
||
get "/t/#{topic_id}/wordpress.json", params: { best: 1 }
|
||
end,
|
||
"GET /t/:topic_id/post_ids.json" => ->(topic_id) { get "/t/#{topic_id}/post_ids.json" },
|
||
"GET /t/:topic_id/posts.json" => ->(topic_id) { get "/t/#{topic_id}/posts.json" },
|
||
"PUT /t/:id/archive-message.json" => ->(topic_id) do
|
||
put "/t/#{topic_id}/archive-message.json"
|
||
end,
|
||
"PUT /t/:id/move-to-inbox.json" => ->(topic_id) { put "/t/#{topic_id}/move-to-inbox.json" },
|
||
"PUT /t/:id/publish.json" => ->(topic_id) do
|
||
put "/t/#{topic_id}/publish.json", params: { destination_category_id: category.id }
|
||
end,
|
||
"PUT /t/:topic_id/slow_mode.json" => ->(topic_id) do
|
||
put "/t/#{topic_id}/slow_mode.json", params: { seconds: "3600" }
|
||
end,
|
||
"POST /t/:topic_id/notifications.json" => ->(topic_id) do
|
||
post "/t/#{topic_id}/notifications.json",
|
||
params: {
|
||
notification_level: NotificationLevels.topic_levels[:watching],
|
||
}
|
||
end,
|
||
}
|
||
|
||
requests.each do |description, perform_request|
|
||
perform_request.call(private_message.id)
|
||
expect(response.status).to eq(404), description
|
||
expect(response.parsed_body["error_type"]).to eq("not_found"), description
|
||
|
||
perform_request.call(nonexistent_topic_id)
|
||
expect(response.status).to eq(404), description
|
||
expect(response.parsed_body["error_type"]).to eq("not_found"), description
|
||
end
|
||
end
|
||
end
|
||
|
||
describe "#post_ids" do
|
||
fab!(:post) { Fabricate(:post, user: post_author1) }
|
||
fab!(:topic) { post.topic }
|
||
|
||
before { TopicView.stubs(:chunk_size).returns(1) }
|
||
|
||
it "returns the right post ids" do
|
||
post2 = Fabricate(:post, user: post_author2, topic: topic)
|
||
post3 = Fabricate(:post, user: post_author3, topic: topic)
|
||
|
||
get "/t/#{topic.id}/post_ids.json", params: { post_number: post.post_number }
|
||
|
||
expect(response.status).to eq(200)
|
||
|
||
body = response.parsed_body
|
||
|
||
expect(body["post_ids"]).to eq([post2.id, post3.id])
|
||
end
|
||
|
||
describe "filtering by post number with filters" do
|
||
describe "username filters" do
|
||
fab!(:post) { Fabricate(:post, user: user) }
|
||
fab!(:post2) { Fabricate(:post, topic: topic, user: user) }
|
||
fab!(:post3) { Fabricate(:post, user: post_author3, topic: topic) }
|
||
|
||
it "should return the right posts" do
|
||
get "/t/#{topic.id}/post_ids.json",
|
||
params: {
|
||
post_number: post.post_number,
|
||
username_filters: post2.user.username,
|
||
}
|
||
|
||
expect(response.status).to eq(200)
|
||
|
||
body = response.parsed_body
|
||
|
||
expect(body["post_ids"]).to eq([post2.id])
|
||
end
|
||
end
|
||
|
||
describe "summary filter" do
|
||
fab!(:post2) { Fabricate(:post, user: post_author2, topic: topic, percent_rank: 0.2) }
|
||
fab!(:post3) { Fabricate(:post, user: post_author3, topic: topic) }
|
||
|
||
it "should return the right posts" do
|
||
get "/t/#{topic.id}/post_ids.json",
|
||
params: {
|
||
post_number: post.post_number,
|
||
filter: "summary",
|
||
}
|
||
|
||
expect(response.status).to eq(200)
|
||
|
||
body = response.parsed_body
|
||
|
||
expect(body["post_ids"]).to eq([post2.id])
|
||
end
|
||
end
|
||
|
||
describe "custom filters" do
|
||
fab!(:post2) { Fabricate(:post, user: post_author2, topic: topic, percent_rank: 0.2) }
|
||
fab!(:post3) { Fabricate(:post, user: post_author3, topic: topic, percent_rank: 0.5) }
|
||
|
||
after { TopicView.custom_filters.clear }
|
||
|
||
it "should return the right posts" do
|
||
TopicView.add_custom_filter("percent") do |posts, topic_view|
|
||
posts.where(percent_rank: 0.5)
|
||
end
|
||
|
||
get "/t/#{topic.id}.json", params: { post_number: post.post_number, filter: "percent" }
|
||
|
||
expect(response.status).to eq(200)
|
||
|
||
body = response.parsed_body
|
||
|
||
expect(body["post_stream"]["posts"].map { |p| p["id"] }).to eq([post3.id])
|
||
end
|
||
end
|
||
end
|
||
end
|
||
|
||
describe "#posts" do
|
||
fab!(:post) { Fabricate(:post, user: post_author1) }
|
||
fab!(:topic) { post.topic }
|
||
|
||
after { Discourse.redis.flushdb }
|
||
|
||
it "returns first post of the topic" do
|
||
# we need one for suggested
|
||
create_post
|
||
|
||
get "/t/#{topic.id}/posts.json"
|
||
|
||
expect(response.status).to eq(200)
|
||
|
||
body = response.parsed_body
|
||
|
||
expect(body["post_stream"]["posts"].first["id"]).to eq(post.id)
|
||
|
||
expect(body["suggested_topics"]).to eq(nil)
|
||
|
||
get "/t/#{topic.id}/posts.json?include_suggested=true"
|
||
body = response.parsed_body
|
||
|
||
expect(body["suggested_topics"]).not_to eq(nil)
|
||
end
|
||
|
||
it "omits reply-to user names when names are disabled" do
|
||
SiteSetting.enable_names = false
|
||
post.user.update!(name: "Hidden Reply Target")
|
||
reply =
|
||
Fabricate(
|
||
:post,
|
||
topic: topic,
|
||
user: post_author2,
|
||
reply_to_post_number: post.post_number,
|
||
reply_to_user_id: post.user_id,
|
||
)
|
||
|
||
get "/t/#{topic.id}/posts.json", params: { post_ids: [reply.id] }
|
||
|
||
expect(response.status).to eq(200)
|
||
posts = response.parsed_body["post_stream"]["posts"]
|
||
reply_post = posts.find { |post_json| post_json["id"] == reply.id }
|
||
reply_to_user = reply_post["reply_to_user"]
|
||
|
||
expect(reply_to_user).to include("id" => post.user_id, "username" => post.user.username)
|
||
expect(reply_to_user).not_to have_key("name")
|
||
expect(response.body).not_to include(post.user.name)
|
||
end
|
||
|
||
it "optionally can return raw" do
|
||
get "/t/#{topic.id}/posts.json?include_raw=true&post_id[]=#{post.id}"
|
||
|
||
expect(response.status).to eq(200)
|
||
|
||
body = response.parsed_body
|
||
|
||
expect(body["post_stream"]["posts"].first["raw"]).to eq(post.raw)
|
||
end
|
||
|
||
describe "filtering by post number with filters" do
|
||
describe "username filters" do
|
||
fab!(:post2) { Fabricate(:post, user: post_author2, topic: topic) }
|
||
fab!(:post3) { Fabricate(:post, user: post_author3, topic: topic) }
|
||
|
||
it "should return the right posts" do
|
||
TopicView.stubs(:chunk_size).returns(2)
|
||
|
||
get "/t/#{topic.id}/posts.json",
|
||
params: {
|
||
post_number: post.post_number,
|
||
username_filters: post2.user.username,
|
||
asc: true,
|
||
}
|
||
|
||
expect(response.status).to eq(200)
|
||
|
||
body = response.parsed_body
|
||
|
||
expect(body["post_stream"]["posts"].first["id"]).to eq(post2.id)
|
||
end
|
||
end
|
||
|
||
describe "summary filter" do
|
||
fab!(:post2) { Fabricate(:post, user: post_author2, topic: topic, percent_rank: 0.2) }
|
||
fab!(:post3) { Fabricate(:post, user: post_author3, topic: topic) }
|
||
|
||
it "should return the right posts" do
|
||
TopicView.stubs(:chunk_size).returns(2)
|
||
|
||
get "/t/#{topic.id}/posts.json",
|
||
params: {
|
||
post_number: post.post_number,
|
||
filter: "summary",
|
||
asc: true,
|
||
}
|
||
|
||
expect(response.status).to eq(200)
|
||
|
||
body = response.parsed_body
|
||
|
||
expect(body["post_stream"]["posts"].first["id"]).to eq(post2.id)
|
||
end
|
||
end
|
||
end
|
||
|
||
describe "#posts with content localization" do
|
||
fab!(:localized_post) do
|
||
post = Fabricate(:post, user:, locale: "en", cooked: "<p>Original EN</p>")
|
||
Fabricate(:post_localization, post:, locale: "ja", cooked: "<p>Translated JA</p>")
|
||
post
|
||
end
|
||
fab!(:localized_topic) { localized_post.topic }
|
||
fab!(:localized_post2) do
|
||
post =
|
||
Fabricate(
|
||
:post,
|
||
user:,
|
||
topic: localized_topic,
|
||
locale: "ja",
|
||
cooked: "<p>Original 2 JA</p>",
|
||
)
|
||
Fabricate(:post_localization, post:, locale: "en", cooked: "<p>Translated 2 EN</p>")
|
||
post
|
||
end
|
||
|
||
before do
|
||
SiteSetting.content_localization_enabled = true
|
||
I18n.locale = "en"
|
||
end
|
||
|
||
context "when show_original cookie is not set" do
|
||
it "returns translated posts" do
|
||
get "/t/#{localized_topic.id}/posts.json"
|
||
|
||
expect(response.status).to eq(200)
|
||
|
||
body = response.parsed_body
|
||
posts = body["post_stream"]["posts"]
|
||
|
||
expect(posts.first["cooked"]).to eq("<p>Original EN</p>")
|
||
expect(posts.second["cooked"]).to eq("<p>Translated 2 EN</p>")
|
||
end
|
||
|
||
it "returns translated posts when loading specific post_ids" do
|
||
get "/t/#{localized_topic.id}/posts.json", params: { post_ids: [localized_post2.id] }
|
||
|
||
expect(response.status).to eq(200)
|
||
|
||
body = response.parsed_body
|
||
posts = body["post_stream"]["posts"]
|
||
|
||
expect(posts.first["cooked"]).to eq("<p>Translated 2 EN</p>")
|
||
end
|
||
end
|
||
|
||
context "when show_original cookie is set" do
|
||
before { cookies[ContentLocalization::SHOW_ORIGINAL_COOKIE] = "true" }
|
||
|
||
it "returns original posts" do
|
||
get "/t/#{localized_topic.id}/posts.json"
|
||
|
||
expect(response.status).to eq(200)
|
||
|
||
body = response.parsed_body
|
||
posts = body["post_stream"]["posts"]
|
||
|
||
expect(posts.first["cooked"]).to eq("<p>Original EN</p>")
|
||
expect(posts.second["cooked"]).to eq("<p>Original 2 JA</p>")
|
||
end
|
||
|
||
it "returns original posts when loading specific post_ids" do
|
||
get "/t/#{localized_topic.id}/posts.json", params: { post_ids: [localized_post2.id] }
|
||
|
||
expect(response.status).to eq(200)
|
||
|
||
body = response.parsed_body
|
||
posts = body["post_stream"]["posts"]
|
||
|
||
expect(posts.first["cooked"]).to eq("<p>Original 2 JA</p>")
|
||
end
|
||
end
|
||
end
|
||
end
|
||
|
||
describe "#feed" do
|
||
fab!(:topic) { Fabricate(:post, user: post_author1).topic }
|
||
|
||
it "renders rss of the topic" do
|
||
get "/t/foo/#{topic.id}.rss"
|
||
expect(response.status).to eq(200)
|
||
expect(response.media_type).to eq("application/rss+xml")
|
||
|
||
# our RSS feed is full of post 1/2/3/4/5 links, we do not want it included
|
||
# in the index, and do not want links followed
|
||
# this allows us to remove it while allowing via robots.txt
|
||
expect(response.headers["X-Robots-Tag"]).to eq("noindex, nofollow")
|
||
end
|
||
|
||
it "removes invalid characters from the feed" do
|
||
topic.title = "This is a big topic title with a "
|
||
topic.save!
|
||
|
||
get "/t/foo/#{topic.id}.rss"
|
||
expect(response.status).to eq(200)
|
||
expect(response.body).to_not include("")
|
||
end
|
||
|
||
it "renders rss of the topic correctly with subfolder" do
|
||
set_subfolder "/forum"
|
||
get "/t/foo/#{topic.id}.rss"
|
||
expect(response.status).to eq(200)
|
||
expect(response.body).to_not include("/forum/forum")
|
||
expect(response.body).to include("http://test.localhost/forum/t/#{topic.slug}")
|
||
end
|
||
|
||
it "returns 404 when posts are deleted" do
|
||
topic.posts.each(&:trash!)
|
||
get "/t/foo/#{topic.id}.rss"
|
||
expect(response.status).to eq(404)
|
||
end
|
||
|
||
it "returns 404 when the topic is deleted" do
|
||
topic.trash!
|
||
get "/t/foo/#{topic.id}.rss"
|
||
expect(response.status).to eq(404)
|
||
end
|
||
end
|
||
|
||
describe "#make_banner" do
|
||
it "needs you to be a staff member" do
|
||
tl4_topic = Fabricate(:topic, user: sign_in(trust_level_4))
|
||
put "/t/#{tl4_topic.id}/make-banner.json"
|
||
expect(response).to be_forbidden
|
||
end
|
||
|
||
describe "when logged in" do
|
||
it "changes the topic archetype to 'banner'" do
|
||
admin_topic = Fabricate(:topic, user: sign_in(admin))
|
||
|
||
put "/t/#{admin_topic.id}/make-banner.json"
|
||
expect(response.status).to eq(200)
|
||
admin_topic.reload
|
||
expect(admin_topic.archetype).to eq(Archetype.banner)
|
||
end
|
||
end
|
||
end
|
||
|
||
describe "#remove_banner" do
|
||
it "needs you to be a staff member" do
|
||
tl4_topic = Fabricate(:topic, user: sign_in(trust_level_4), archetype: Archetype.banner)
|
||
put "/t/#{tl4_topic.id}/remove-banner.json"
|
||
expect(response).to be_forbidden
|
||
end
|
||
|
||
describe "when logged in" do
|
||
it "resets the topic archetype" do
|
||
admin_topic = Fabricate(:topic, user: sign_in(admin), archetype: Archetype.banner)
|
||
|
||
put "/t/#{admin_topic.id}/remove-banner.json"
|
||
expect(response.status).to eq(200)
|
||
admin_topic.reload
|
||
expect(admin_topic.archetype).to eq(Archetype.default)
|
||
end
|
||
end
|
||
end
|
||
|
||
describe "#remove_allowed_user" do
|
||
it "admin can be removed from a pm" do
|
||
sign_in(admin)
|
||
pm =
|
||
create_post(
|
||
user: user,
|
||
archetype: "private_message",
|
||
target_usernames: [user.username, admin.username],
|
||
)
|
||
|
||
put "/t/#{pm.topic_id}/remove-allowed-user.json", params: { username: admin.username }
|
||
|
||
expect(response.status).to eq(200)
|
||
expect(TopicAllowedUser.where(topic_id: pm.topic_id, user_id: admin.id).first).to eq(nil)
|
||
end
|
||
end
|
||
|
||
describe "#bulk" do
|
||
it "needs you to be logged in" do
|
||
put "/topics/bulk.json"
|
||
expect(response.status).to eq(403)
|
||
end
|
||
|
||
describe "when logged in" do
|
||
fab!(:topic_2, :topic)
|
||
|
||
before { sign_in(user) }
|
||
let!(:operation) { { type: "change_category", category_id: "1", silent: true } }
|
||
let!(:topic_ids) { [1, 2, 3] }
|
||
|
||
it "requires a list of topic_ids or filter" do
|
||
put "/topics/bulk.json", params: { operation: operation }
|
||
expect(response.status).to eq(400)
|
||
end
|
||
|
||
it "requires an operation param" do
|
||
put "/topics/bulk.json", params: { topic_ids: topic_ids }
|
||
expect(response.status).to eq(400)
|
||
end
|
||
|
||
it "requires a type field for the operation param" do
|
||
put "/topics/bulk.json", params: { topic_ids: topic_ids, operation: {} }
|
||
expect(response.status).to eq(400)
|
||
end
|
||
|
||
it "returns a proper error for an invalid operation type" do
|
||
put "/topics/bulk.json",
|
||
params: {
|
||
topic_ids: topic_ids,
|
||
operation: {
|
||
type: "not_a_real_operation",
|
||
},
|
||
}
|
||
|
||
expect(response.status).to eq(400)
|
||
expect(response.parsed_body["errors"]).to be_present
|
||
end
|
||
|
||
it "can dismiss sub-categories posts as read" do
|
||
sub = Fabricate(:category, parent_category_id: category.id)
|
||
|
||
topic.update!(category_id: sub.id)
|
||
|
||
post1 = create_post(user: user, topic_id: topic.id)
|
||
create_post(topic_id: topic.id)
|
||
|
||
put "/topics/bulk.json",
|
||
params: {
|
||
category_id: category.id,
|
||
include_subcategories: true,
|
||
filter: "unread",
|
||
operation: {
|
||
type: "dismiss_posts",
|
||
},
|
||
}
|
||
|
||
expect(response.status).to eq(200)
|
||
expect(TopicUser.get(post1.topic, post1.user).last_read_post_number).to eq(2)
|
||
end
|
||
|
||
it "can dismiss sub-subcategories posts as read" do
|
||
SiteSetting.max_category_nesting = 3
|
||
|
||
sub_category = Fabricate(:category, parent_category_id: category.id)
|
||
sub_subcategory = Fabricate(:category, parent_category_id: sub_category.id)
|
||
|
||
topic.update!(category_id: sub_subcategory.id)
|
||
|
||
post_1 = create_post(user: user, topic_id: topic.id)
|
||
_post_2 = create_post(topic_id: topic.id)
|
||
|
||
put "/topics/bulk.json",
|
||
params: {
|
||
category_id: category.id,
|
||
include_subcategories: true,
|
||
filter: "unread",
|
||
operation: {
|
||
type: "dismiss_posts",
|
||
},
|
||
}
|
||
|
||
expect(response.status).to eq(200)
|
||
expect(TopicUser.get(post_1.topic, post_1.user).last_read_post_number).to eq(2)
|
||
end
|
||
|
||
it "can mark tag topics unread" do
|
||
TopicTag.create!(topic_id: topic.id, tag_id: tag.id)
|
||
|
||
post1 = create_post(user: user, topic_id: topic.id)
|
||
create_post(topic_id: topic.id)
|
||
|
||
put "/topics/bulk.json",
|
||
params: {
|
||
tag_name: tag.name,
|
||
filter: "unread",
|
||
operation: {
|
||
type: "dismiss_posts",
|
||
},
|
||
}
|
||
|
||
expect(response.status).to eq(200)
|
||
expect(TopicUser.get(post1.topic, post1.user).last_read_post_number).to eq(2)
|
||
end
|
||
|
||
it "can append tags" do
|
||
SiteSetting.tagging_enabled = true
|
||
SiteSetting.tag_topic_allowed_groups = Group::AUTO_GROUPS[:trust_level_0]
|
||
tag1 = Fabricate(:tag)
|
||
topic.update!(user:)
|
||
_first_post = Fabricate(:post, topic:, user:)
|
||
|
||
put "/topics/bulk.json",
|
||
params: {
|
||
topic_ids: [topic.id],
|
||
operation: {
|
||
type: "append_tags",
|
||
tag_ids: [tag1.id],
|
||
},
|
||
}
|
||
|
||
expect(response.status).to eq(200)
|
||
expect(topic.reload.tags).to include(tag1)
|
||
end
|
||
|
||
it "can append tags with tag names for backward compatibility" do
|
||
SiteSetting.tagging_enabled = true
|
||
SiteSetting.tag_topic_allowed_groups = Group::AUTO_GROUPS[:trust_level_0]
|
||
tag1 = Fabricate(:tag)
|
||
topic.update!(user:)
|
||
_first_post = Fabricate(:post, topic:, user:)
|
||
|
||
put "/topics/bulk.json",
|
||
params: {
|
||
topic_ids: [topic.id],
|
||
operation: {
|
||
type: "append_tags",
|
||
tags: [tag1.name],
|
||
},
|
||
}
|
||
|
||
expect(response.status).to eq(200)
|
||
expect(topic.reload.tags).to include(tag1)
|
||
end
|
||
|
||
it "can manage tags via add, remove and replace across multiple topics" do
|
||
SiteSetting.tagging_enabled = true
|
||
SiteSetting.tag_topic_allowed_groups = Group::AUTO_GROUPS[:trust_level_0]
|
||
tag_1 = Fabricate(:tag)
|
||
tag_2 = Fabricate(:tag)
|
||
tag_3 = Fabricate(:tag)
|
||
tag_4 = Fabricate(:tag)
|
||
topic_1 = Fabricate(:topic_with_op, user: user, tags: [tag_1, tag_2])
|
||
topic_2 = Fabricate(:topic_with_op, user: user, tags: [tag_2, tag_3])
|
||
topic_3 = Fabricate(:topic_with_op, user: user, tags: [tag_1])
|
||
|
||
put "/topics/bulk.json",
|
||
params: {
|
||
topic_ids: [topic_1.id, topic_2.id, topic_3.id],
|
||
operation: {
|
||
type: "manage_tags",
|
||
add_tag_ids: [tag_4.id],
|
||
remove_tag_ids: [tag_2.id],
|
||
replace_tags: [{ from_tag_id: tag_1.id, to_tag_id: tag_3.id }],
|
||
},
|
||
}
|
||
|
||
expect(response.status).to eq(200)
|
||
expect(topic_1.reload.tags).to contain_exactly(tag_3, tag_4)
|
||
expect(topic_2.reload.tags).to contain_exactly(tag_3, tag_4)
|
||
expect(topic_3.reload.tags).to contain_exactly(tag_3, tag_4)
|
||
end
|
||
|
||
it "can clear all tags with remove_all_tags across multiple topics" do
|
||
SiteSetting.tagging_enabled = true
|
||
SiteSetting.tag_topic_allowed_groups = Group::AUTO_GROUPS[:trust_level_0]
|
||
tag_1 = Fabricate(:tag)
|
||
tag_2 = Fabricate(:tag)
|
||
topic_1 = Fabricate(:topic_with_op, user: user, tags: [tag_1, tag_2])
|
||
topic_2 = Fabricate(:topic_with_op, user: user, tags: [tag_2])
|
||
|
||
put "/topics/bulk.json",
|
||
params: {
|
||
topic_ids: [topic_1.id, topic_2.id],
|
||
operation: {
|
||
type: "manage_tags",
|
||
remove_all_tags: true,
|
||
},
|
||
}
|
||
|
||
expect(response.status).to eq(200)
|
||
expect(topic_1.reload.tags).to be_empty
|
||
expect(topic_2.reload.tags).to be_empty
|
||
end
|
||
|
||
it "includes errors in the response when operations partially fail" do
|
||
sign_in(Fabricate(:moderator))
|
||
|
||
restricted_tag = Fabricate(:tag, name: "restricted-tag")
|
||
source_category = Fabricate(:category, tags: [restricted_tag])
|
||
destination_category = Fabricate(:category, tags: [Fabricate(:tag, name: "other-tag")])
|
||
topic_with_tag = Fabricate(:topic, category: source_category, tags: [restricted_tag])
|
||
Fabricate(:post, topic: topic_with_tag)
|
||
|
||
put "/topics/bulk.json",
|
||
params: {
|
||
topic_ids: [topic_with_tag.id],
|
||
operation: {
|
||
type: "change_category",
|
||
category_id: destination_category.id,
|
||
},
|
||
}
|
||
|
||
expect(response.status).to eq(200)
|
||
json = response.parsed_body
|
||
expect(json["topic_ids"]).to eq([])
|
||
expect(json["errors"]).to be_present
|
||
expect(json["errors"].values.sum).to eq(1)
|
||
end
|
||
|
||
context "with private message" do
|
||
fab!(:group) do
|
||
Fabricate(:group, messageable_level: Group::ALIAS_LEVELS[:everyone]).tap do |g|
|
||
g.add(user_2)
|
||
end
|
||
end
|
||
|
||
fab!(:group_message) do
|
||
create_post(
|
||
user: user,
|
||
target_group_names: [group.name],
|
||
archetype: Archetype.private_message,
|
||
).topic
|
||
end
|
||
|
||
fab!(:private_message) do
|
||
create_post(
|
||
user: user,
|
||
target_usernames: [user_2.username],
|
||
archetype: Archetype.private_message,
|
||
).topic
|
||
end
|
||
|
||
fab!(:private_message_2) do
|
||
create_post(
|
||
user: user,
|
||
target_usernames: [user_2.username],
|
||
archetype: Archetype.private_message,
|
||
).topic
|
||
end
|
||
|
||
fab!(:group_pm_topic_user) do
|
||
TopicUser
|
||
.find_by(user: user_2, topic: group_message)
|
||
.tap { |tu| tu.update!(last_read_post_number: 1) }
|
||
end
|
||
|
||
fab!(:regular_pm_topic_user) do
|
||
TopicUser
|
||
.find_by(user: user_2, topic: private_message)
|
||
.tap { |tu| tu.update!(last_read_post_number: 1) }
|
||
end
|
||
|
||
fab!(:regular_pm_topic_user_2) do
|
||
TopicUser
|
||
.find_by(user: user_2, topic: private_message_2)
|
||
.tap { |tu| tu.update!(last_read_post_number: 1) }
|
||
end
|
||
|
||
before_all do
|
||
create_post(user: user, topic: group_message)
|
||
create_post(user: user, topic: private_message)
|
||
create_post(user: user, topic: private_message_2)
|
||
end
|
||
|
||
before { sign_in(user_2) }
|
||
|
||
it "can dismiss all user and group private message topics" do
|
||
expect do
|
||
put "/topics/bulk.json",
|
||
params: {
|
||
filter: "unread",
|
||
operation: {
|
||
type: "dismiss_posts",
|
||
},
|
||
private_message_inbox: "all",
|
||
}
|
||
|
||
expect(response.status).to eq(200)
|
||
end.to change { group_pm_topic_user.reload.last_read_post_number }.from(1).to(
|
||
2,
|
||
).and change { regular_pm_topic_user.reload.last_read_post_number }.from(1).to(2)
|
||
end
|
||
|
||
it "can dismiss all user unread private message topics" do
|
||
stub_const(TopicQuery, "DEFAULT_PER_PAGE_COUNT", 1) do
|
||
expect do
|
||
put "/topics/bulk.json",
|
||
params: {
|
||
filter: "unread",
|
||
operation: {
|
||
type: "dismiss_posts",
|
||
},
|
||
private_message_inbox: "user",
|
||
}
|
||
|
||
expect(response.status).to eq(200)
|
||
end.to change { regular_pm_topic_user.reload.last_read_post_number }.from(1).to(
|
||
2,
|
||
).and change { regular_pm_topic_user_2.reload.last_read_post_number }.from(1).to(2)
|
||
|
||
expect(group_pm_topic_user.reload.last_read_post_number).to eq(1)
|
||
end
|
||
end
|
||
|
||
it "returns the right response when trying to dismiss private messages of an invalid group" do
|
||
put "/topics/bulk.json",
|
||
params: {
|
||
filter: "unread",
|
||
operation: {
|
||
type: "dismiss_posts",
|
||
},
|
||
private_message_inbox: "group",
|
||
group_name: "randomgroup",
|
||
}
|
||
|
||
expect(response.status).to eq(404)
|
||
end
|
||
|
||
it "returns the right response when trying to dismiss private messages of a restricted group" do
|
||
sign_in(user)
|
||
|
||
put "/topics/bulk.json",
|
||
params: {
|
||
filter: "unread",
|
||
operation: {
|
||
type: "dismiss_posts",
|
||
},
|
||
private_message_inbox: "group",
|
||
group_name: group.name,
|
||
}
|
||
|
||
expect(response.status).to eq(404)
|
||
end
|
||
|
||
it "can dismiss all group unread private message topics" do
|
||
expect do
|
||
put "/topics/bulk.json",
|
||
params: {
|
||
filter: "unread",
|
||
operation: {
|
||
type: "dismiss_posts",
|
||
},
|
||
private_message_inbox: "group",
|
||
group_name: group.name,
|
||
}
|
||
|
||
expect(response.status).to eq(200)
|
||
end.to change { group_pm_topic_user.reload.last_read_post_number }.from(1).to(2)
|
||
|
||
expect(regular_pm_topic_user.reload.last_read_post_number).to eq(1)
|
||
end
|
||
end
|
||
|
||
it "can find unread" do
|
||
# mark all unread muted
|
||
put "/topics/bulk.json",
|
||
params: {
|
||
filter: "unread",
|
||
operation: {
|
||
type: :change_notification_level,
|
||
notification_level_id: 0,
|
||
},
|
||
}
|
||
|
||
expect(response.status).to eq(200)
|
||
end
|
||
|
||
it "delegates work to `TopicsBulkAction`" do
|
||
topics_bulk_action = mock
|
||
TopicsBulkAction
|
||
.expects(:new)
|
||
.with(user, topic_ids, operation, group: nil)
|
||
.returns(topics_bulk_action)
|
||
topics_bulk_action.expects(:perform!)
|
||
|
||
put "/topics/bulk.json", params: { topic_ids: topic_ids, operation: operation }
|
||
end
|
||
|
||
it "raises an error if topic_ids is provided and it is not an array" do
|
||
put "/topics/bulk.json", params: { topic_ids: "1", operation: operation }
|
||
expect(response.parsed_body["errors"].first).to match(
|
||
/Expecting topic_ids to contain a list/,
|
||
)
|
||
put "/topics/bulk.json", params: { topic_ids: [1], operation: operation }
|
||
expect(response.parsed_body["errors"]).to eq(nil)
|
||
end
|
||
|
||
it "deduplicates explicit topic IDs before processing them" do
|
||
sign_in(trust_level_0)
|
||
|
||
put "/topics/bulk.json",
|
||
params: {
|
||
topic_ids: Array.new(5, 0),
|
||
operation: {
|
||
type: "dismiss_posts",
|
||
},
|
||
},
|
||
as: :json
|
||
|
||
expect(response.status).to eq(200)
|
||
expect(response.parsed_body["topic_ids"].size).to eq(1)
|
||
expect(response.parsed_body["topic_ids"].first).to eq(0)
|
||
end
|
||
|
||
it "rejects more than 1,000 unique explicit topic IDs" do
|
||
sign_in(trust_level_0)
|
||
|
||
put "/topics/bulk.json",
|
||
params: {
|
||
topic_ids: (1..1_001).to_a,
|
||
operation: {
|
||
type: "dismiss_posts",
|
||
},
|
||
},
|
||
as: :json
|
||
|
||
expect(response.status).to eq(400)
|
||
expect(response.parsed_body["errors"].first).to include(
|
||
I18n.t("topics_bulk_action.too_many_topic_ids", limit: 1_000),
|
||
)
|
||
end
|
||
|
||
it "can pin multiple topics with pinned_until" do
|
||
sign_in(moderator)
|
||
pinned_until = 3.days.from_now.beginning_of_minute.iso8601
|
||
|
||
put "/topics/bulk.json",
|
||
params: {
|
||
topic_ids: [topic.id, topic_2.id],
|
||
operation: {
|
||
type: "pin",
|
||
pinned_globally: false,
|
||
pinned_until: pinned_until,
|
||
},
|
||
}
|
||
|
||
expect(response.status).to eq(200)
|
||
expect(response.parsed_body["topic_ids"]).to contain_exactly(topic.id, topic_2.id)
|
||
expect(topic.reload.pinned_until).to be_within_one_second_of(Time.parse(pinned_until))
|
||
expect(topic_2.reload.pinned_until).to be_within_one_second_of(Time.parse(pinned_until))
|
||
end
|
||
|
||
it "can unpin multiple topics" do
|
||
sign_in(moderator)
|
||
topic.update_pinned(true, true)
|
||
topic_2.update_pinned(true, false)
|
||
|
||
put "/topics/bulk.json",
|
||
params: {
|
||
topic_ids: [topic.id, topic_2.id],
|
||
operation: {
|
||
type: "unpin",
|
||
},
|
||
}
|
||
|
||
expect(response.status).to eq(200)
|
||
expect(response.parsed_body["topic_ids"]).to contain_exactly(topic.id, topic_2.id)
|
||
end
|
||
|
||
it "respects the tracked parameter" do
|
||
# untracked topic
|
||
CategoryUser.set_notification_level_for_category(
|
||
user,
|
||
NotificationLevels.all[:regular],
|
||
category.id,
|
||
)
|
||
create_post(user: user, topic_id: topic.id)
|
||
topic.update!(category_id: category.id)
|
||
create_post(topic_id: topic.id)
|
||
|
||
# tracked topic
|
||
CategoryUser.set_notification_level_for_category(
|
||
user,
|
||
NotificationLevels.all[:tracking],
|
||
tracked_category.id,
|
||
)
|
||
tracked_topic = create_post(user: user).topic
|
||
tracked_topic.update!(category_id: tracked_category.id)
|
||
create_post(topic_id: tracked_topic.id)
|
||
|
||
put "/topics/bulk.json",
|
||
params: {
|
||
filter: "unread",
|
||
operation: {
|
||
type: "dismiss_posts",
|
||
},
|
||
tracked: true,
|
||
}
|
||
|
||
expect(response.status).to eq(200)
|
||
expect(TopicUser.get(topic, user).last_read_post_number).to eq(topic.posts.count - 1)
|
||
expect(TopicUser.get(tracked_topic, user).last_read_post_number).to eq(
|
||
tracked_topic.posts.count,
|
||
)
|
||
end
|
||
end
|
||
end
|
||
|
||
describe "#remove_bookmarks" do
|
||
it "requires the user to be logged in" do
|
||
put "/t/1/remove_bookmarks.json"
|
||
expect(response.status).to eq(403)
|
||
end
|
||
|
||
it "returns 404 for inaccessible private messages" do
|
||
sign_in(user_2)
|
||
private_message =
|
||
create_post(
|
||
user: user,
|
||
archetype: "private_message",
|
||
target_usernames: [user.username],
|
||
).topic
|
||
missing_topic_id = Topic.maximum(:id) + 1
|
||
|
||
put "/t/#{private_message.id}/remove_bookmarks.json"
|
||
expect(response.status).to eq(404)
|
||
expect(response.parsed_body["error_type"]).to eq("not_found")
|
||
|
||
put "/t/#{missing_topic_id}/remove_bookmarks.json"
|
||
expect(response.status).to eq(404)
|
||
expect(response.parsed_body["error_type"]).to eq("not_found")
|
||
end
|
||
|
||
it "should remove bookmarks properly from non first post" do
|
||
sign_in(user)
|
||
|
||
post = create_post
|
||
post2 = create_post(topic_id: post.topic_id)
|
||
Fabricate(:bookmark, user: user, bookmarkable: post)
|
||
Fabricate(:bookmark, user: user, bookmarkable: post2)
|
||
|
||
put "/t/#{post.topic_id}/remove_bookmarks.json"
|
||
expect(Bookmark.where(user: user).count).to eq(0)
|
||
end
|
||
|
||
context "with bookmarks with reminders" do
|
||
it "deletes all the bookmarks for the user in the topic" do
|
||
sign_in(user)
|
||
post = create_post
|
||
Fabricate(:bookmark, bookmarkable: post, user: user)
|
||
put "/t/#{post.topic_id}/remove_bookmarks.json"
|
||
expect(Bookmark.for_user_in_topic(user.id, post.topic_id).count).to eq(0)
|
||
end
|
||
end
|
||
end
|
||
|
||
describe "#bookmark" do
|
||
before { sign_in(user) }
|
||
|
||
it "returns 404 for inaccessible private messages" do
|
||
sign_in(user_2)
|
||
private_message =
|
||
create_post(
|
||
user: user,
|
||
archetype: "private_message",
|
||
target_usernames: [user.username],
|
||
).topic
|
||
missing_topic_id = Topic.maximum(:id) + 1
|
||
|
||
put "/t/#{private_message.id}/bookmark.json"
|
||
expect(response.status).to eq(404)
|
||
expect(response.parsed_body["error_type"]).to eq("not_found")
|
||
|
||
put "/t/#{missing_topic_id}/bookmark.json"
|
||
expect(response.status).to eq(404)
|
||
expect(response.parsed_body["error_type"]).to eq("not_found")
|
||
end
|
||
|
||
it "should create a new bookmark for the topic" do
|
||
post = create_post
|
||
_post2 = create_post(topic_id: post.topic_id)
|
||
put "/t/#{post.topic_id}/bookmark.json"
|
||
|
||
expect(Bookmark.find_by(user_id: user.id).bookmarkable_id).to eq(post.topic_id)
|
||
end
|
||
|
||
it "errors if the topic is already bookmarked for the user" do
|
||
post = create_post
|
||
Bookmark.create(bookmarkable: post.topic, user: user)
|
||
|
||
put "/t/#{post.topic_id}/bookmark.json"
|
||
expect(response.status).to eq(400)
|
||
end
|
||
end
|
||
|
||
describe "#reset_new" do
|
||
context "when a user is not signed in" do
|
||
it "fails" do
|
||
put "/topics/reset-new.json"
|
||
expect(response.status).to eq(403)
|
||
end
|
||
end
|
||
|
||
context "when a user is signed in" do
|
||
before_all do
|
||
@old_date = 2.years.ago
|
||
user.user_stat.update_column(:new_since, @old_date)
|
||
|
||
CategoryUser.set_notification_level_for_category(
|
||
user,
|
||
NotificationLevels.all[:tracking],
|
||
tracked_category.id,
|
||
)
|
||
end
|
||
|
||
let!(:old_date) { @old_date }
|
||
|
||
before { sign_in(user) }
|
||
|
||
context "when tracked is unset" do
|
||
it "updates the `new_since` date" do
|
||
TopicTrackingState.expects(:publish_dismiss_new).never
|
||
|
||
put "/topics/reset-new.json"
|
||
expect(response.status).to eq(200)
|
||
user.reload
|
||
expect(user.user_stat.new_since.to_date).not_to eq(old_date.to_date)
|
||
end
|
||
end
|
||
|
||
describe "when tracked param is true" do
|
||
it "does not update user_stat.new_since" do
|
||
put "/topics/reset-new.json?tracked=true"
|
||
expect(response.status).to eq(200)
|
||
user.reload
|
||
expect(user.user_stat.new_since.to_date).to eq(old_date.to_date)
|
||
end
|
||
|
||
it "creates dismissed topic user records for each new topic" do
|
||
tracked_topic = create_post(category: tracked_category).topic
|
||
|
||
create_post # This is a new post, but is not tracked so a record will not be created for it
|
||
expect do
|
||
put "/topics/reset-new.json?tracked=true", params: { dismiss_topics: true }
|
||
end.to change {
|
||
DismissedTopicUser.where(user_id: user.id, topic_id: tracked_topic.id).count
|
||
}.by(1)
|
||
end
|
||
end
|
||
|
||
context "when 5 tracked topics exist" do
|
||
before_all do
|
||
@tracked_topic_ids = 5.times.map { create_post(category: tracked_category).topic.id }
|
||
@tracked_topic_ids.freeze
|
||
end
|
||
|
||
describe "when tracked param is true" do
|
||
it "creates dismissed topic user records if there are > 30 (default pagination) topics" do
|
||
expect do
|
||
stub_const(TopicQuery, "DEFAULT_PER_PAGE_COUNT", 2) do
|
||
put "/topics/reset-new.json?tracked=true", params: { dismiss_topics: true }
|
||
end
|
||
end.to change {
|
||
DismissedTopicUser.where(user_id: user.id, topic_id: @tracked_topic_ids).count
|
||
}.by(5)
|
||
end
|
||
|
||
it "creates dismissed topic user records if there are > 30 (default pagination) topics and topic_ids are provided" do
|
||
dismissing_topic_ids = @tracked_topic_ids.sample(4)
|
||
|
||
expect do
|
||
stub_const(TopicQuery, "DEFAULT_PER_PAGE_COUNT", 2) do
|
||
put "/topics/reset-new.json?tracked=true",
|
||
params: {
|
||
dismiss_topics: true,
|
||
topic_ids: dismissing_topic_ids,
|
||
}
|
||
end
|
||
end.to change {
|
||
DismissedTopicUser.where(user_id: user.id, topic_id: @tracked_topic_ids).count
|
||
}.by(4)
|
||
end
|
||
end
|
||
|
||
context "when two extra topics exist" do
|
||
before_all do
|
||
@topic_ids = @tracked_topic_ids + [Fabricate(:topic).id, Fabricate(:topic).id]
|
||
@topic_ids.freeze
|
||
end
|
||
|
||
context "when tracked=false" do
|
||
it "updates the user_stat new_since column and dismisses all the new topics" do
|
||
old_new_since = user.user_stat.new_since
|
||
|
||
put "/topics/reset-new.json?tracked=false", params: { dismiss_topics: true }
|
||
expect(DismissedTopicUser.where(user_id: user.id, topic_id: @topic_ids).count).to eq(
|
||
7,
|
||
)
|
||
expect(user.reload.user_stat.new_since > old_new_since).to eq(true)
|
||
end
|
||
|
||
it "does not pass topic ids that are not new for the user to the bulk action, limit the scope to new topics" do
|
||
dismiss_ids = @topic_ids[0..1]
|
||
|
||
DismissedTopicUser.create(user_id: user.id, topic_id: dismiss_ids.first)
|
||
DismissedTopicUser.create(user_id: user.id, topic_id: dismiss_ids.second)
|
||
|
||
expect do
|
||
put "/topics/reset-new.json?tracked=false", params: { dismiss_topics: true }
|
||
end.to change { DismissedTopicUser.where(user_id: user.id).count }.by(5)
|
||
end
|
||
end
|
||
end
|
||
end
|
||
|
||
context "with category" do
|
||
fab!(:subcategory) { Fabricate(:category, parent_category_id: category.id) }
|
||
fab!(:category_topic) { Fabricate(:topic, category: category) }
|
||
fab!(:subcategory_topic) { Fabricate(:topic, category: subcategory) }
|
||
|
||
it "dismisses topics for main category" do
|
||
TopicTrackingState.expects(:publish_dismiss_new).with(
|
||
user.id,
|
||
topic_ids: [category_topic.id],
|
||
)
|
||
|
||
put "/topics/reset-new.json?category_id=#{category.id}", params: { dismiss_topics: true }
|
||
|
||
expect(DismissedTopicUser.where(user_id: user.id).pluck(:topic_id)).to eq(
|
||
[category_topic.id],
|
||
)
|
||
end
|
||
|
||
it "dismisses topics for main category and subcategories" do
|
||
TopicTrackingState.expects(:publish_dismiss_new).with(
|
||
user.id,
|
||
topic_ids: [category_topic.id, subcategory_topic.id],
|
||
)
|
||
|
||
put "/topics/reset-new.json?category_id=#{category.id}&include_subcategories=true",
|
||
params: {
|
||
dismiss_topics: true,
|
||
}
|
||
|
||
expect(response.status).to eq(200)
|
||
|
||
expect(DismissedTopicUser.where(user_id: user.id).pluck(:topic_id).sort).to eq(
|
||
[category_topic.id, subcategory_topic.id].sort,
|
||
)
|
||
end
|
||
|
||
it "dismisses topics for main category, subcategories and sub-subcategories" do
|
||
SiteSetting.max_category_nesting = 3
|
||
|
||
sub_subcategory = Fabricate(:category, parent_category_id: subcategory.id)
|
||
sub_subcategory_topic = Fabricate(:topic, category: sub_subcategory)
|
||
|
||
TopicTrackingState.expects(:publish_dismiss_new).with(
|
||
user.id,
|
||
topic_ids: [category_topic.id, subcategory_topic.id, sub_subcategory_topic.id],
|
||
)
|
||
|
||
put "/topics/reset-new.json?category_id=#{category.id}&include_subcategories=true",
|
||
params: {
|
||
dismiss_topics: true,
|
||
}
|
||
|
||
expect(response.status).to eq(200)
|
||
|
||
expect(DismissedTopicUser.where(user_id: user.id).pluck(:topic_id)).to contain_exactly(
|
||
category_topic.id,
|
||
subcategory_topic.id,
|
||
sub_subcategory_topic.id,
|
||
)
|
||
end
|
||
|
||
context "when the category has private child categories" do
|
||
fab!(:category)
|
||
fab!(:group)
|
||
fab!(:private_child_category) do
|
||
Fabricate(:private_category, parent_category: category, group: group)
|
||
end
|
||
fab!(:public_child_category) { Fabricate(:category, parent_category: category) }
|
||
fab!(:topic_in_private_child_category) do
|
||
Fabricate(:topic, category: private_child_category)
|
||
end
|
||
fab!(:topic_in_public_child_category) do
|
||
Fabricate(:topic, category: public_child_category)
|
||
end
|
||
|
||
it "doesn't dismiss topics in private child categories that the user can't see" do
|
||
messages =
|
||
MessageBus.track_publish(TopicTrackingState.unread_channel_key(user.id)) do
|
||
put "/topics/reset-new.json",
|
||
params: {
|
||
dismiss_topics: true,
|
||
category_id: category.id,
|
||
include_subcategories: true,
|
||
}
|
||
|
||
expect(response.status).to eq(200)
|
||
end
|
||
|
||
expect(messages.size).to eq(1)
|
||
expect(messages[0].user_ids).to eq([user.id])
|
||
expect(messages[0].data["message_type"]).to eq(
|
||
TopicTrackingState::DISMISS_NEW_MESSAGE_TYPE,
|
||
)
|
||
expect(messages[0].data["payload"]["topic_ids"]).to eq(
|
||
[topic_in_public_child_category.id],
|
||
)
|
||
expect(DismissedTopicUser.where(user_id: user.id).pluck(:topic_id)).to eq(
|
||
[topic_in_public_child_category.id],
|
||
)
|
||
end
|
||
|
||
it "dismisses topics in private child categories that the user can see" do
|
||
group.add(user)
|
||
|
||
messages =
|
||
MessageBus.track_publish(TopicTrackingState.unread_channel_key(user.id)) do
|
||
put "/topics/reset-new.json",
|
||
params: {
|
||
dismiss_topics: true,
|
||
category_id: category.id,
|
||
include_subcategories: true,
|
||
}
|
||
|
||
expect(response.status).to eq(200)
|
||
end
|
||
|
||
expect(messages.size).to eq(1)
|
||
expect(messages[0].user_ids).to eq([user.id])
|
||
expect(messages[0].data["message_type"]).to eq(
|
||
TopicTrackingState::DISMISS_NEW_MESSAGE_TYPE,
|
||
)
|
||
expect(messages[0].data["payload"]["topic_ids"]).to contain_exactly(
|
||
topic_in_public_child_category.id,
|
||
topic_in_private_child_category.id,
|
||
)
|
||
expect(DismissedTopicUser.where(user_id: user.id).pluck(:topic_id)).to contain_exactly(
|
||
topic_in_public_child_category.id,
|
||
topic_in_private_child_category.id,
|
||
)
|
||
end
|
||
end
|
||
|
||
context "when the category is private" do
|
||
fab!(:group)
|
||
fab!(:private_category) { Fabricate(:private_category, group: group) }
|
||
fab!(:topic_in_private_category) { Fabricate(:topic, category: private_category) }
|
||
|
||
it "doesn't dismiss topics or publish topic IDs via MessageBus if the user can't access the category" do
|
||
messages =
|
||
MessageBus.track_publish do
|
||
put "/topics/reset-new.json",
|
||
params: {
|
||
dismiss_topics: true,
|
||
category_id: private_category.id,
|
||
}
|
||
expect(response.status).to eq(200)
|
||
end
|
||
|
||
expect(messages.size).to eq(0)
|
||
expect(DismissedTopicUser.where(user_id: user.id).count).to eq(0)
|
||
end
|
||
|
||
it "dismisses topics and publishes the dismissed topic IDs if the user can access the category" do
|
||
group.add(user)
|
||
messages =
|
||
MessageBus.track_publish do
|
||
put "/topics/reset-new.json",
|
||
params: {
|
||
dismiss_topics: true,
|
||
category_id: private_category.id,
|
||
}
|
||
end
|
||
expect(response.status).to eq(200)
|
||
expect(messages.size).to eq(1)
|
||
expect(messages[0].channel).to eq(TopicTrackingState.unread_channel_key(user.id))
|
||
expect(messages[0].user_ids).to eq([user.id])
|
||
expect(messages[0].data["message_type"]).to eq(
|
||
TopicTrackingState::DISMISS_NEW_MESSAGE_TYPE,
|
||
)
|
||
expect(messages[0].data["payload"]["topic_ids"]).to eq([topic_in_private_category.id])
|
||
expect(DismissedTopicUser.where(user_id: user.id).pluck(:topic_id)).to eq(
|
||
[topic_in_private_category.id],
|
||
)
|
||
end
|
||
end
|
||
end
|
||
|
||
context "with tag" do
|
||
fab!(:tag_topic, :topic)
|
||
fab!(:topic_tag) { Fabricate(:topic_tag, topic: tag_topic, tag: tag) }
|
||
|
||
it "dismisses topics for tag" do
|
||
TopicTrackingState.expects(:publish_dismiss_new).with(user.id, topic_ids: [tag_topic.id])
|
||
put "/topics/reset-new.json?tag_name=#{tag.name}", params: { dismiss_topics: true }
|
||
expect(DismissedTopicUser.where(user_id: user.id).pluck(:topic_id)).to eq([tag_topic.id])
|
||
end
|
||
|
||
context "when the tag is restricted" do
|
||
fab!(:restricted_tag) { Fabricate(:tag, name: "restricted-tag") }
|
||
fab!(:topic_with_restricted_tag) { Fabricate(:topic, tags: [restricted_tag]) }
|
||
fab!(:group)
|
||
fab!(:topic_without_tag, :topic)
|
||
fab!(:tag_group) do
|
||
Fabricate(
|
||
:tag_group,
|
||
name: "Restricted Tag Group",
|
||
tag_names: ["restricted-tag"],
|
||
permissions: [[group, TagGroupPermission.permission_types[:full]]],
|
||
)
|
||
end
|
||
|
||
it "respects the tag param and only dismisses topics tagged with this tag if the user can see it" do
|
||
group.add(user)
|
||
messages =
|
||
MessageBus.track_publish do
|
||
put "/topics/reset-new.json",
|
||
params: {
|
||
dismiss_topics: true,
|
||
tag_name: restricted_tag.name,
|
||
}
|
||
end
|
||
expect(messages.size).to eq(1)
|
||
expect(messages[0].data["payload"]["topic_ids"]).to contain_exactly(
|
||
topic_with_restricted_tag.id,
|
||
)
|
||
expect(DismissedTopicUser.where(user_id: user.id).pluck(:topic_id)).to contain_exactly(
|
||
topic_with_restricted_tag.id,
|
||
)
|
||
end
|
||
|
||
it "ignores the tag param and dismisses all topics if the user can't see the tag" do
|
||
messages =
|
||
MessageBus.track_publish do
|
||
put "/topics/reset-new.json",
|
||
params: {
|
||
dismiss_topics: true,
|
||
tag_name: restricted_tag.name,
|
||
}
|
||
end
|
||
expect(messages.size).to eq(1)
|
||
expect(messages[0].data["payload"]["topic_ids"]).to contain_exactly(
|
||
topic_with_restricted_tag.id,
|
||
tag_topic.id,
|
||
topic_without_tag.id,
|
||
)
|
||
expect(DismissedTopicUser.where(user_id: user.id).pluck(:topic_id)).to contain_exactly(
|
||
topic_with_restricted_tag.id,
|
||
tag_topic.id,
|
||
topic_without_tag.id,
|
||
)
|
||
end
|
||
end
|
||
end
|
||
|
||
context "with tag and category" do
|
||
fab!(:tag_topic, :topic)
|
||
fab!(:topic_tag) { Fabricate(:topic_tag, topic: tag_topic, tag: tag) }
|
||
fab!(:tag_and_category_topic) { Fabricate(:topic, category: category) }
|
||
fab!(:topic_tag2) { Fabricate(:topic_tag, topic: tag_and_category_topic, tag: tag) }
|
||
|
||
it "dismisses topics for tag" do
|
||
TopicTrackingState.expects(:publish_dismiss_new).with(
|
||
user.id,
|
||
topic_ids: [tag_and_category_topic.id],
|
||
)
|
||
put "/topics/reset-new.json?tag_name=#{tag.name}&category_id=#{category.id}",
|
||
params: {
|
||
dismiss_topics: true,
|
||
}
|
||
expect(DismissedTopicUser.where(user_id: user.id).pluck(:topic_id)).to eq(
|
||
[tag_and_category_topic.id],
|
||
)
|
||
end
|
||
end
|
||
|
||
context "with specific topics" do
|
||
fab!(:topic2, :topic)
|
||
fab!(:topic3, :topic)
|
||
|
||
it "updates the `new_since` date" do
|
||
TopicTrackingState
|
||
.expects(:publish_dismiss_new)
|
||
.with(user.id, topic_ids: [topic2.id, topic3.id])
|
||
.at_least_once
|
||
|
||
put "/topics/reset-new.json",
|
||
**{ params: { dismiss_topics: true, topic_ids: [topic2.id, topic3.id] } }
|
||
expect(response.status).to eq(200)
|
||
user.reload
|
||
expect(user.user_stat.new_since.to_date).not_to eq(old_date.to_date)
|
||
expect(DismissedTopicUser.where(user_id: user.id).pluck(:topic_id)).to match_array(
|
||
[topic2.id, topic3.id],
|
||
)
|
||
end
|
||
|
||
it "raises an error if topic_ids is provided and it is not an array" do
|
||
put "/topics/reset-new.json", params: { topic_ids: topic2.id }
|
||
expect(response.parsed_body["errors"].first).to match(
|
||
/Expecting topic_ids to contain a list/,
|
||
)
|
||
put "/topics/reset-new.json", params: { topic_ids: [topic2.id] }
|
||
expect(response.parsed_body["errors"]).to eq(nil)
|
||
end
|
||
|
||
it "doesn't dismiss topics that the user can't see" do
|
||
private_category = Fabricate(:private_category, group: Fabricate(:group))
|
||
topic2.update!(category_id: private_category.id)
|
||
|
||
messages =
|
||
MessageBus.track_publish do
|
||
put "/topics/reset-new.json",
|
||
params: {
|
||
dismiss_topics: true,
|
||
topic_ids: [topic2.id, topic3.id],
|
||
}
|
||
end
|
||
expect(messages.size).to eq(1)
|
||
expect(messages[0].channel).to eq(TopicTrackingState.unread_channel_key(user.id))
|
||
expect(messages[0].user_ids).to eq([user.id])
|
||
expect(messages[0].data["message_type"]).to eq(
|
||
TopicTrackingState::DISMISS_NEW_MESSAGE_TYPE,
|
||
)
|
||
expect(messages[0].data["payload"]["topic_ids"]).to eq([topic3.id])
|
||
expect(DismissedTopicUser.where(user_id: user.id).pluck(:topic_id)).to eq([topic3.id])
|
||
end
|
||
|
||
describe "when tracked param is true" do
|
||
it "does not update user_stat.new_since and does not dismiss untracked topics" do
|
||
put "/topics/reset-new.json?tracked=true",
|
||
**{ params: { topic_ids: [topic2.id, topic3.id] } }
|
||
expect(response.status).to eq(200)
|
||
user.reload
|
||
expect(user.user_stat.new_since.to_date).to eq(old_date.to_date)
|
||
expect(DismissedTopicUser.where(user_id: user.id).pluck(:topic_id)).to be_empty
|
||
end
|
||
|
||
it "creates topic user records for each unread topic" do
|
||
tracked_topic = create_post.topic
|
||
tracked_topic.update!(category_id: tracked_category.id)
|
||
topic2.update!(category_id: tracked_category.id)
|
||
|
||
create_post # This is a new post, but is not tracked so a record will not be created for it
|
||
expect do
|
||
put "/topics/reset-new.json?tracked=true",
|
||
**{
|
||
params: {
|
||
dismiss_topics: true,
|
||
topic_ids: [tracked_topic.id, topic2.id, topic3.id],
|
||
},
|
||
}
|
||
end.to change { DismissedTopicUser.where(user_id: user.id).count }.by(2)
|
||
expect(DismissedTopicUser.where(user_id: user.id).pluck(:topic_id)).to match_array(
|
||
[tracked_topic.id, topic2.id],
|
||
)
|
||
end
|
||
end
|
||
end
|
||
end
|
||
|
||
describe "new and unread" do
|
||
fab!(:group)
|
||
fab!(:new_topic, :topic)
|
||
fab!(:unread_topic) { Fabricate(:topic, highest_post_number: 3) }
|
||
fab!(:topic_user) do
|
||
Fabricate(
|
||
:topic_user,
|
||
topic: unread_topic,
|
||
user: user,
|
||
notification_level: NotificationLevels.topic_levels[:tracking],
|
||
last_read_post_number: 1,
|
||
)
|
||
end
|
||
|
||
before do
|
||
create_post(topic: unread_topic)
|
||
create_post(topic: unread_topic)
|
||
SiteSetting.enable_unified_new = true
|
||
sign_in(user)
|
||
end
|
||
|
||
it "dismisses new topics" do
|
||
put "/topics/reset-new.json"
|
||
topics = TopicQuery.new(user).new_and_unread_results(limit: false)
|
||
expect(topics).to eq([unread_topic, new_topic])
|
||
expect(response.status).to eq(200)
|
||
expect(response.parsed_body["topic_ids"]).to eq([])
|
||
|
||
put "/topics/reset-new.json", params: { dismiss_topics: true }
|
||
expect(response.status).to eq(200)
|
||
expect(response.parsed_body["topic_ids"]).to eq([new_topic.id])
|
||
|
||
topics = TopicQuery.new(user).new_and_unread_results(limit: false)
|
||
expect(topics).to eq([unread_topic])
|
||
expect(DismissedTopicUser.where(user: user).count).to eq(1)
|
||
expect(DismissedTopicUser.where(user: user).first.topic_id).to eq(new_topic.id)
|
||
expect(topic_user.reload.notification_level).to eq(
|
||
NotificationLevels.topic_levels[:tracking],
|
||
)
|
||
end
|
||
|
||
it "dismisses unread topics" do
|
||
put "/topics/reset-new.json"
|
||
expect(response.status).to eq(200)
|
||
expect(response.parsed_body["topic_ids"]).to eq([])
|
||
topics = TopicQuery.new(user).new_and_unread_results(limit: false)
|
||
expect(topics).to eq([unread_topic, new_topic])
|
||
|
||
put "/topics/reset-new.json", params: { dismiss_posts: true }
|
||
expect(response.status).to eq(200)
|
||
expect(response.parsed_body["topic_ids"]).to eq([unread_topic.id])
|
||
|
||
topics = TopicQuery.new(user).new_and_unread_results(limit: false)
|
||
expect(topics).to eq([new_topic])
|
||
expect(DismissedTopicUser.count).to eq(0)
|
||
expect(topic_user.reload.notification_level).to eq(
|
||
NotificationLevels.topic_levels[:tracking],
|
||
)
|
||
end
|
||
|
||
it "untrack topics" do
|
||
expect(topic_user.notification_level).to eq(NotificationLevels.topic_levels[:tracking])
|
||
put "/topics/reset-new.json", params: { dismiss_posts: true, untrack: true }
|
||
expect(response.status).to eq(200)
|
||
expect(response.parsed_body["topic_ids"]).to eq([unread_topic.id])
|
||
|
||
expect(topic_user.reload.notification_level).to eq(
|
||
NotificationLevels.topic_levels[:regular],
|
||
)
|
||
end
|
||
|
||
it "dismisses new topics, unread posts and untrack" do
|
||
put "/topics/reset-new.json",
|
||
params: {
|
||
dismiss_topics: true,
|
||
dismiss_posts: true,
|
||
untrack: true,
|
||
}
|
||
expect(response.status).to eq(200)
|
||
expect(response.parsed_body["topic_ids"]).to eq([new_topic.id, unread_topic.id])
|
||
|
||
topics = TopicQuery.new(user).new_and_unread_results(limit: false)
|
||
expect(topics).to be_empty
|
||
expect(DismissedTopicUser.where(user: user).count).to eq(1)
|
||
expect(DismissedTopicUser.where(user: user).first.topic_id).to eq(new_topic.id)
|
||
|
||
expect(user.topic_users.map(&:notification_level).uniq).to eq(
|
||
[NotificationLevels.topic_levels[:regular]],
|
||
)
|
||
end
|
||
|
||
context "when category" do
|
||
fab!(:category)
|
||
fab!(:new_topic_2) { Fabricate(:topic, category: category) }
|
||
fab!(:unread_topic_2) { Fabricate(:topic, category: category, highest_post_number: 3) }
|
||
fab!(:topic_user) do
|
||
Fabricate(
|
||
:topic_user,
|
||
topic: unread_topic_2,
|
||
user: user,
|
||
notification_level: NotificationLevels.topic_levels[:tracking],
|
||
last_read_post_number: 1,
|
||
)
|
||
end
|
||
|
||
it "dismisses new topics, unread posts and untrack for specific category" do
|
||
topics = TopicQuery.new(user).new_and_unread_results(limit: false)
|
||
expect(topics).to match_array([new_topic, new_topic_2, unread_topic, unread_topic_2])
|
||
|
||
put "/topics/reset-new.json",
|
||
params: {
|
||
dismiss_topics: true,
|
||
dismiss_posts: true,
|
||
untrack: true,
|
||
category_id: category.id,
|
||
}
|
||
expect(response.status).to eq(200)
|
||
expect(response.parsed_body["topic_ids"]).to eq([new_topic_2.id, unread_topic_2.id])
|
||
|
||
topics = TopicQuery.new(user).new_and_unread_results(limit: false)
|
||
expect(topics).to match_array([new_topic, unread_topic])
|
||
end
|
||
end
|
||
|
||
context "when tag" do
|
||
fab!(:tag)
|
||
fab!(:new_topic_2, :topic)
|
||
fab!(:unread_topic_2) { Fabricate(:topic, highest_post_number: 3) }
|
||
fab!(:topic_user) do
|
||
Fabricate(
|
||
:topic_user,
|
||
topic: unread_topic_2,
|
||
user: user,
|
||
notification_level: NotificationLevels.topic_levels[:tracking],
|
||
last_read_post_number: 1,
|
||
)
|
||
end
|
||
fab!(:topic_tag) { Fabricate(:topic_tag, topic: new_topic_2, tag: tag) }
|
||
fab!(:topic_tag_2) { Fabricate(:topic_tag, topic: unread_topic_2, tag: tag) }
|
||
|
||
it "dismisses new topics, unread posts and untrack for specific tag" do
|
||
topics = TopicQuery.new(user).new_and_unread_results(limit: false)
|
||
expect(topics).to match_array([new_topic, new_topic_2, unread_topic, unread_topic_2])
|
||
|
||
put "/topics/reset-new.json",
|
||
params: {
|
||
dismiss_topics: true,
|
||
dismiss_posts: true,
|
||
untrack: true,
|
||
tag_name: tag.name,
|
||
}
|
||
|
||
expect(response.status).to eq(200)
|
||
expect(response.parsed_body["topic_ids"]).to eq([new_topic_2.id, unread_topic_2.id])
|
||
|
||
topics = TopicQuery.new(user).new_and_unread_results(limit: false)
|
||
expect(topics).to match_array([new_topic, unread_topic])
|
||
end
|
||
end
|
||
end
|
||
end
|
||
|
||
describe "#feature_stats" do
|
||
fab!(:category_for_stats, :category)
|
||
fab!(:pinned_in_category_topic) do
|
||
Fabricate(:topic, category: category_for_stats, pinned_at: 1.hour.ago, pinned_globally: false)
|
||
end
|
||
fab!(:globally_pinned_topic) { Fabricate(:topic, pinned_at: 1.hour.ago, pinned_globally: true) }
|
||
fab!(:banner_topic) { Fabricate(:topic, archetype: Archetype.banner) }
|
||
|
||
it "returns category and global pin counts when category_id is provided" do
|
||
get "/topics/feature_stats.json", params: { category_id: category_for_stats.id }
|
||
|
||
expect(response.status).to eq(200)
|
||
json = response.parsed_body
|
||
expect(json["pinned_in_category_count"]).to eq(1)
|
||
expect(json["pinned_globally_count"]).to eq(1)
|
||
expect(json["banner_count"]).to eq(1)
|
||
end
|
||
|
||
it "returns only global pin and banner counts when category_id is omitted" do
|
||
get "/topics/feature_stats.json"
|
||
|
||
expect(response.status).to eq(200)
|
||
json = response.parsed_body
|
||
expect(json).not_to have_key("pinned_in_category_count")
|
||
expect(json["pinned_globally_count"]).to eq(1)
|
||
expect(json["banner_count"]).to eq(1)
|
||
end
|
||
|
||
it "allows unlisted banner topic" do
|
||
banner_topic.update!(visible: false)
|
||
|
||
get "/topics/feature_stats.json", params: { category_id: category_for_stats.id }
|
||
json = response.parsed_body
|
||
expect(json["banner_count"]).to eq(1)
|
||
end
|
||
|
||
it "does not count topics in read-restricted categories for anonymous users" do
|
||
restricted_category = Fabricate(:category, read_restricted: true)
|
||
Fabricate(
|
||
:topic,
|
||
category: restricted_category,
|
||
pinned_at: 1.hour.ago,
|
||
pinned_globally: false,
|
||
)
|
||
Fabricate(:topic, category: restricted_category, pinned_at: 1.hour.ago, pinned_globally: true)
|
||
Fabricate(:topic, category: restricted_category, archetype: Archetype.banner)
|
||
|
||
get "/topics/feature_stats.json", params: { category_id: restricted_category.id }
|
||
expect(response.status).to eq(200)
|
||
json = response.parsed_body
|
||
expect(json["pinned_in_category_count"]).to eq(0)
|
||
expect(json["pinned_globally_count"]).to eq(1)
|
||
expect(json["banner_count"]).to eq(1)
|
||
end
|
||
end
|
||
|
||
describe "#excerpts" do
|
||
it "can correctly get excerpts" do
|
||
first_post =
|
||
create_post(raw: "This is the first post :)", title: "This is a test title I am making yay")
|
||
second_post = create_post(raw: "This is second post", topic: first_post.topic)
|
||
third_post = first_post.topic.add_small_action(first_post.user, "autobumped")
|
||
|
||
random_post = Fabricate(:post, user: post_author1)
|
||
|
||
get "/t/#{first_post.topic_id}/excerpts.json",
|
||
params: {
|
||
post_ids: [first_post.id, second_post.id, third_post.id, random_post.id],
|
||
}
|
||
|
||
json = response.parsed_body
|
||
json.sort! { |a, b| a["post_id"] <=> b["post_id"] }
|
||
|
||
# no random post
|
||
expect(json.map { |p| p["post_id"] }).to contain_exactly(
|
||
first_post.id,
|
||
second_post.id,
|
||
third_post.id,
|
||
)
|
||
# keep emoji images
|
||
expect(json[0]["excerpt"]).to match(/emoji/)
|
||
expect(json[0]["excerpt"]).to match(/first post/)
|
||
expect(json[0]["username"]).to eq(first_post.user.username)
|
||
expect(json[0]["created_at"].present?).to eq(false)
|
||
|
||
expect(json[1]["excerpt"]).to match(/second post/)
|
||
|
||
expect(json[2]["action_code"]).to eq("autobumped")
|
||
expect(json[2]["created_at"].present?).to eq(true)
|
||
end
|
||
|
||
it "does not return whisper posts to non-staff users" do
|
||
SiteSetting.whispers_allowed_groups = "#{Group::AUTO_GROUPS[:staff]}"
|
||
first_post = create_post(raw: "This is the first post")
|
||
whisper_post =
|
||
create_post(
|
||
raw: "This is a secret whisper",
|
||
topic: first_post.topic,
|
||
post_type: Post.types[:whisper],
|
||
)
|
||
|
||
sign_in(user)
|
||
|
||
get "/t/#{first_post.topic_id}/excerpts.json",
|
||
params: {
|
||
post_ids: [first_post.id, whisper_post.id],
|
||
}
|
||
|
||
json = response.parsed_body
|
||
expect(json.map { |p| p["post_id"] }).to contain_exactly(first_post.id)
|
||
end
|
||
end
|
||
|
||
describe "#convert_topic" do
|
||
it "needs you to be logged in" do
|
||
put "/t/111/convert-topic/private.json"
|
||
expect(response.status).to eq(403)
|
||
end
|
||
|
||
describe "converting public topic to private message" do
|
||
fab!(:topic) { Fabricate(:topic, user: user) }
|
||
fab!(:post) { Fabricate(:post, user: user, topic: topic) }
|
||
|
||
it "raises an error when the user doesn't have permission to convert topic" do
|
||
sign_in(user)
|
||
put "/t/#{topic.id}/convert-topic/private.json"
|
||
expect(response).to be_forbidden
|
||
end
|
||
|
||
context "when the PM recipient cap would be exceeded" do
|
||
fab!(:reply_1) { Fabricate(:post, topic: topic, user: post_author1, post_number: 2) }
|
||
fab!(:reply_2) { Fabricate(:post, topic: topic, user: post_author2, post_number: 3) }
|
||
|
||
before { SiteSetting.max_allowed_message_recipients = 2 }
|
||
|
||
it "returns an error" do
|
||
sign_in(admin)
|
||
put "/t/#{topic.id}/convert-topic/private.json"
|
||
|
||
expect(response.status).to eq(422)
|
||
expect(response.parsed_body["errors"]).to contain_exactly(
|
||
I18n.t(
|
||
"topic_converter.too_many_recipients",
|
||
max: SiteSetting.max_allowed_message_recipients,
|
||
),
|
||
)
|
||
expect(topic.reload.archetype).to eq(Archetype.default)
|
||
end
|
||
end
|
||
|
||
context "with success" do
|
||
it "returns success" do
|
||
sign_in(admin)
|
||
put "/t/#{topic.id}/convert-topic/private.json"
|
||
|
||
topic.reload
|
||
expect(topic.archetype).to eq(Archetype.private_message)
|
||
expect(response.status).to eq(200)
|
||
|
||
result = response.parsed_body
|
||
expect(result["success"]).to eq(true)
|
||
expect(result["url"]).to be_present
|
||
end
|
||
end
|
||
end
|
||
|
||
describe "converting private message to public topic" do
|
||
fab!(:topic) { Fabricate(:private_message_topic, user: user) }
|
||
fab!(:post) { Fabricate(:post, user: post_author1, topic: topic) }
|
||
|
||
it "raises an error when the user doesn't have permission to convert topic" do
|
||
sign_in(user)
|
||
put "/t/#{topic.id}/convert-topic/public.json"
|
||
expect(response).to be_forbidden
|
||
end
|
||
|
||
it "raises an error when a moderator doesn't have permission to convert topic" do
|
||
sign_in(moderator)
|
||
put "/t/#{topic.id}/convert-topic/public.json"
|
||
expect(response).to be_forbidden
|
||
end
|
||
|
||
context "with success" do
|
||
it "returns success and the new url" do
|
||
sign_in(admin)
|
||
put "/t/#{topic.id}/convert-topic/public.json?category_id=#{category.id}"
|
||
|
||
topic.reload
|
||
expect(topic.archetype).to eq(Archetype.default)
|
||
expect(topic.category_id).to eq(category.id)
|
||
expect(response.status).to eq(200)
|
||
|
||
result = response.parsed_body
|
||
expect(result["success"]).to eq(true)
|
||
expect(result["url"]).to be_present
|
||
end
|
||
end
|
||
|
||
context "with some errors" do
|
||
it "returns the error messages" do
|
||
existing_topic = Fabricate(:topic, title: topic.title, category: category)
|
||
|
||
sign_in(admin)
|
||
put "/t/#{topic.id}/convert-topic/public.json?category_id=#{category.id}"
|
||
|
||
expect(response.status).to eq(422)
|
||
expect(response.parsed_body["errors"][0]).to end_with(
|
||
I18n.t("errors.messages.topic_title_already_used", url: existing_topic.url),
|
||
)
|
||
end
|
||
end
|
||
end
|
||
end
|
||
|
||
describe "#timings" do
|
||
fab!(:post_1) { Fabricate(:post, user: post_author1, topic: topic) }
|
||
|
||
before do
|
||
# admins
|
||
SiteSetting.whispers_allowed_groups = "1"
|
||
end
|
||
|
||
let(:whisper) do
|
||
Fabricate(:post, user: post_author1, topic: topic, post_type: Post.types[:whisper])
|
||
end
|
||
|
||
it "should gracefully handle invalid timings sent in" do
|
||
sign_in(user)
|
||
params = {
|
||
topic_id: topic.id,
|
||
topic_time: 5,
|
||
timings: {
|
||
post_1.post_number => 2,
|
||
whisper.post_number => 2,
|
||
1000 => 100,
|
||
},
|
||
}
|
||
|
||
post "/topics/timings.json", params: params
|
||
expect(response.status).to eq(200)
|
||
|
||
tu = TopicUser.find_by(user: user, topic: topic)
|
||
expect(tu.last_read_post_number).to eq(post_1.post_number)
|
||
|
||
# lets also test timing recovery here
|
||
tu.update!(last_read_post_number: 999)
|
||
|
||
post "/topics/timings.json", params: params
|
||
|
||
tu = TopicUser.find_by(user: user, topic: topic)
|
||
expect(tu.last_read_post_number).to eq(post_1.post_number)
|
||
end
|
||
|
||
it "should gracefully handle invalid timings sent in from staff" do
|
||
sign_in(admin)
|
||
|
||
post "/topics/timings.json",
|
||
params: {
|
||
topic_id: topic.id,
|
||
topic_time: 5,
|
||
timings: {
|
||
post_1.post_number => 2,
|
||
whisper.post_number => 2,
|
||
1000 => 100,
|
||
},
|
||
}
|
||
|
||
expect(response.status).to eq(200)
|
||
|
||
tu = TopicUser.find_by(user: admin, topic: topic)
|
||
expect(tu.last_read_post_number).to eq(whisper.post_number)
|
||
end
|
||
|
||
it "should record the timing" do
|
||
sign_in(user)
|
||
|
||
post "/topics/timings.json",
|
||
params: {
|
||
topic_id: topic.id,
|
||
topic_time: 5,
|
||
timings: {
|
||
post_1.post_number => 2,
|
||
},
|
||
}
|
||
|
||
expect(response.status).to eq(200)
|
||
|
||
post_timing = PostTiming.first
|
||
|
||
expect(post_timing.topic).to eq(topic)
|
||
expect(post_timing.user).to eq(user)
|
||
expect(post_timing.msecs).to eq(2)
|
||
end
|
||
|
||
it "caps post read time at the max integer value (2^31 - 1)" do
|
||
PostTiming.create!(
|
||
topic_id: post_1.topic.id,
|
||
post_number: post_1.post_number,
|
||
user_id: user.id,
|
||
msecs: 2**31 - 10,
|
||
)
|
||
sign_in(user)
|
||
|
||
post "/topics/timings.json",
|
||
params: {
|
||
topic_id: topic.id,
|
||
topic_time: 5,
|
||
timings: {
|
||
post_1.post_number => 100,
|
||
},
|
||
}
|
||
|
||
expect(response.status).to eq(200)
|
||
post_timing = PostTiming.first
|
||
|
||
expect(post_timing.topic).to eq(topic)
|
||
expect(post_timing.user).to eq(user)
|
||
expect(post_timing.msecs).to eq(2**31 - 1)
|
||
end
|
||
end
|
||
|
||
describe "#timer" do
|
||
context "when a user is not logged in" do
|
||
it "should return the right response" do
|
||
post "/t/#{topic.id}/timer.json", params: { time: "24", status_type: TopicTimer.types[1] }
|
||
expect(response.status).to eq(403)
|
||
end
|
||
end
|
||
|
||
context "when does not have permission" do
|
||
it "should return the right response" do
|
||
sign_in(user)
|
||
|
||
post "/t/#{topic.id}/timer.json", params: { time: "24", status_type: TopicTimer.types[1] }
|
||
|
||
expect(response.status).to eq(403)
|
||
expect(response.parsed_body["error_type"]).to eq("invalid_access")
|
||
end
|
||
end
|
||
|
||
context "when logged in as a user in the topic timers allowed groups" do
|
||
fab!(:topic_timer_group, :group)
|
||
|
||
before do
|
||
topic_timer_group.add(user)
|
||
user.reload
|
||
SiteSetting.topic_timers_allowed_groups = topic_timer_group.id.to_s
|
||
sign_in(user)
|
||
end
|
||
|
||
it "allows creating a topic timer" do
|
||
post "/t/#{topic.id}/timer.json", params: { time: "24", status_type: TopicTimer.types[1] }
|
||
|
||
expect(response.status).to eq(200)
|
||
expect(topic.reload.public_topic_timer.user).to eq(user)
|
||
end
|
||
|
||
it "requires delete permissions for destructive timers" do
|
||
post "/t/#{topic.id}/timer.json", params: { time: "24", status_type: "delete" }
|
||
|
||
expect(response.status).to eq(403)
|
||
expect(response.parsed_body["error_type"]).to eq("invalid_access")
|
||
expect(topic.reload.public_topic_timer).to eq(nil)
|
||
end
|
||
end
|
||
|
||
context "when time is in the past" do
|
||
it "returns an error" do
|
||
freeze_time
|
||
sign_in(admin)
|
||
|
||
post "/t/#{topic.id}/timer.json",
|
||
params: {
|
||
time: 1.day.ago,
|
||
status_type: TopicTimer.types[1],
|
||
}
|
||
expect(response.status).to eq(400)
|
||
end
|
||
end
|
||
|
||
context "when logged in as an admin" do
|
||
before do
|
||
freeze_time
|
||
sign_in(admin)
|
||
end
|
||
|
||
it "should be able to create a topic status update" do
|
||
post "/t/#{topic.id}/timer.json", params: { time: 24, status_type: TopicTimer.types[1] }
|
||
|
||
expect(response.status).to eq(200)
|
||
|
||
topic_status_update = TopicTimer.last
|
||
|
||
expect(topic_status_update.topic).to eq(topic)
|
||
expect(topic_status_update.execute_at).to eq_time(24.hours.from_now)
|
||
|
||
json = response.parsed_body
|
||
|
||
expect(DateTime.parse(json["execute_at"])).to eq_time(
|
||
DateTime.parse(topic_status_update.execute_at.to_s),
|
||
)
|
||
|
||
expect(json["duration_minutes"]).to eq(topic_status_update.duration_minutes)
|
||
expect(json["closed"]).to eq(topic.reload.closed)
|
||
end
|
||
|
||
it "should be able to delete a topic status update" do
|
||
Fabricate(:topic_timer, topic: topic)
|
||
|
||
post "/t/#{topic.id}/timer.json", params: { time: nil, status_type: TopicTimer.types[1] }
|
||
|
||
expect(response.status).to eq(200)
|
||
expect(topic.reload.public_topic_timer).to eq(nil)
|
||
|
||
json = response.parsed_body
|
||
|
||
expect(json["execute_at"]).to eq(nil)
|
||
expect(json["duration_minutes"]).to eq(nil)
|
||
expect(json["closed"]).to eq(topic.closed)
|
||
end
|
||
|
||
it "should be able to create a topic status update with duration" do
|
||
post "/t/#{topic.id}/timer.json",
|
||
params: {
|
||
duration_minutes: 7200,
|
||
status_type: TopicTimer.types[7],
|
||
}
|
||
|
||
expect(response.status).to eq(200)
|
||
|
||
topic_status_update = TopicTimer.last
|
||
|
||
expect(topic_status_update.topic).to eq(topic)
|
||
expect(topic_status_update.execute_at).to eq_time(5.days.from_now)
|
||
expect(topic_status_update.duration_minutes).to eq(7200)
|
||
|
||
json = response.parsed_body
|
||
|
||
expect(DateTime.parse(json["execute_at"])).to eq_time(
|
||
DateTime.parse(topic_status_update.execute_at.to_s),
|
||
)
|
||
|
||
expect(json["duration_minutes"]).to eq(topic_status_update.duration_minutes)
|
||
end
|
||
|
||
it "should be able to delete a topic status update for delete_replies type" do
|
||
Fabricate(:topic_timer, topic: topic, status_type: TopicTimer.types[:delete_replies])
|
||
|
||
post "/t/#{topic.id}/timer.json", params: { time: nil, status_type: TopicTimer.types[7] }
|
||
|
||
expect(response.status).to eq(200)
|
||
expect(topic.reload.public_topic_timer).to eq(nil)
|
||
|
||
json = response.parsed_body
|
||
|
||
expect(json["execute_at"]).to eq(nil)
|
||
expect(json["duration"]).to eq(nil)
|
||
expect(json["closed"]).to eq(topic.closed)
|
||
end
|
||
|
||
describe "publishing topic to category in the future" do
|
||
it "should be able to create the topic status update" do
|
||
post "/t/#{topic.id}/timer.json",
|
||
params: {
|
||
time: 24,
|
||
status_type: TopicTimer.types[3],
|
||
category_id: topic.category_id,
|
||
}
|
||
|
||
expect(response.status).to eq(200)
|
||
|
||
topic_status_update = TopicTimer.last
|
||
|
||
expect(topic_status_update.topic).to eq(topic)
|
||
expect(topic_status_update.execute_at).to eq_time(24.hours.from_now)
|
||
expect(topic_status_update.status_type).to eq(TopicTimer.types[:publish_to_category])
|
||
|
||
json = response.parsed_body
|
||
|
||
expect(json["category_id"]).to eq(topic.category_id)
|
||
end
|
||
end
|
||
|
||
describe "publishing topic to category without category_id" do
|
||
it "should return an error when setting a timer" do
|
||
post "/t/#{topic.id}/timer.json", params: { time: 24, status_type: "publish_to_category" }
|
||
|
||
expect(response.status).to eq(404)
|
||
end
|
||
|
||
it "should allow removing a timer" do
|
||
topic.set_or_create_timer(
|
||
TopicTimer.types[:publish_to_category],
|
||
24,
|
||
by_user: admin,
|
||
category_id: topic.category_id,
|
||
)
|
||
|
||
post "/t/#{topic.id}/timer.json",
|
||
params: {
|
||
time: nil,
|
||
status_type: "publish_to_category",
|
||
}
|
||
|
||
expect(response.status).to eq(200)
|
||
expect(topic.reload.public_topic_timer).to eq(nil)
|
||
end
|
||
end
|
||
|
||
describe "invalid status type" do
|
||
it "should raise the right error" do
|
||
post "/t/#{topic.id}/timer.json", params: { time: 10, status_type: "something" }
|
||
expect(response.status).to eq(400)
|
||
expect(response.body).to include("status_type")
|
||
end
|
||
end
|
||
end
|
||
|
||
context "when logged in as a TL4 user" do
|
||
before { SiteSetting.enable_category_group_moderation = true }
|
||
it "raises an error if the user can't see the topic" do
|
||
user.update!(trust_level: TrustLevel[4])
|
||
sign_in(user)
|
||
|
||
pm_topic = Fabricate(:private_message_topic)
|
||
|
||
post "/t/#{pm_topic.id}/timer.json",
|
||
params: {
|
||
time: "24",
|
||
status_type: TopicTimer.types[1],
|
||
}
|
||
|
||
expect(response.status).to eq(403)
|
||
expect(response.parsed_body["error_type"]).to eq("invalid_access")
|
||
end
|
||
|
||
it "raises an error when publishing a private message to a category" do
|
||
sign_in(trust_level_4)
|
||
|
||
pm_topic = Fabricate(:private_message_topic, user: trust_level_4)
|
||
|
||
post "/t/#{pm_topic.id}/timer.json",
|
||
params: {
|
||
time: 24,
|
||
status_type: "publish_to_category",
|
||
category_id: category.id,
|
||
}
|
||
|
||
expect(response.status).to eq(403)
|
||
expect(response.parsed_body["error_type"]).to eq("invalid_access")
|
||
expect(pm_topic.reload.public_topic_timer).to eq(nil)
|
||
end
|
||
|
||
it "allows a category moderator to create a delete timer" do
|
||
user.update!(trust_level: TrustLevel[4])
|
||
Group.user_trust_level_change!(user.id, user.trust_level)
|
||
Fabricate(:category_moderation_group, category: topic.category, group: user.groups.first)
|
||
|
||
sign_in(user)
|
||
|
||
post "/t/#{topic.id}/timer.json", params: { time: 10, status_type: "delete" }
|
||
|
||
expect(response.status).to eq(200)
|
||
end
|
||
|
||
it "raises an error setting a delete timer" do
|
||
user.update!(trust_level: TrustLevel[4])
|
||
sign_in(user)
|
||
|
||
post "/t/#{topic.id}/timer.json", params: { time: 10, status_type: "delete" }
|
||
|
||
expect(response.status).to eq(403)
|
||
expect(response.parsed_body["error_type"]).to eq("invalid_access")
|
||
end
|
||
|
||
it "raises an error setting delete_replies timer" do
|
||
user.update!(trust_level: TrustLevel[4])
|
||
sign_in(user)
|
||
|
||
post "/t/#{topic.id}/timer.json", params: { time: 10, status_type: "delete_replies" }
|
||
|
||
expect(response.status).to eq(403)
|
||
expect(response.parsed_body["error_type"]).to eq("invalid_access")
|
||
end
|
||
|
||
it "allows category moderators to set delete_replies timer" do
|
||
user.update!(trust_level: TrustLevel[4])
|
||
Group.user_trust_level_change!(user.id, user.trust_level)
|
||
Fabricate(:category_moderation_group, category: topic.category, group: user.groups.first)
|
||
|
||
sign_in(user)
|
||
|
||
post "/t/#{topic.id}/timer.json",
|
||
params: {
|
||
duration_minutes: 1440,
|
||
status_type: "delete_replies",
|
||
}
|
||
|
||
expect(response.status).to eq(200)
|
||
|
||
topic_timer = TopicTimer.last
|
||
expect(topic_timer.status_type).to eq(TopicTimer.types[:delete_replies])
|
||
end
|
||
|
||
it "raises an error when publishing to a staff-only category" do
|
||
user.update!(trust_level: TrustLevel[4])
|
||
sign_in(user)
|
||
|
||
staff_category = Fabricate(:category)
|
||
staff_category.set_permissions(staff: :full)
|
||
staff_category.save!
|
||
|
||
post "/t/#{topic.id}/timer.json",
|
||
params: {
|
||
time: 24,
|
||
status_type: "publish_to_category",
|
||
category_id: staff_category.id,
|
||
}
|
||
|
||
expect(response.status).to eq(403)
|
||
expect(response.parsed_body["error_type"]).to eq("invalid_access")
|
||
end
|
||
end
|
||
|
||
context "when logged in as a moderator" do
|
||
it "blocks duration-based publishing to a category the moderator cannot create topics in" do
|
||
sign_in(moderator)
|
||
|
||
admin_only_category = Fabricate(:category)
|
||
admin_only_category.set_permissions(admins: :full)
|
||
admin_only_category.save!
|
||
|
||
moderator_guardian = Guardian.new(moderator)
|
||
expect(moderator_guardian.can_moderate?(topic)).to eq(true)
|
||
expect(moderator_guardian.can_create_topic_on_category?(admin_only_category)).to eq(false)
|
||
|
||
post "/t/#{topic.id}/timer.json",
|
||
params: {
|
||
duration_minutes: 60,
|
||
based_on_last_post: true,
|
||
status_type: "publish_to_category",
|
||
category_id: admin_only_category.id,
|
||
}
|
||
|
||
expect(response.status).to eq(403)
|
||
expect(response.parsed_body["error_type"]).to eq("invalid_access")
|
||
expect(topic.reload.public_topic_timer).to eq(nil)
|
||
end
|
||
end
|
||
end
|
||
|
||
describe "#set_slow_mode" do
|
||
context "when not logged in" do
|
||
it "returns a forbidden response" do
|
||
put "/t/#{topic.id}/slow_mode.json", params: { seconds: "3600" }
|
||
|
||
expect(response.status).to eq(403)
|
||
end
|
||
end
|
||
|
||
context "when logged in as an admin" do
|
||
it "allows admins to set the slow mode interval" do
|
||
sign_in(admin)
|
||
|
||
put "/t/#{topic.id}/slow_mode.json", params: { seconds: "3600" }
|
||
|
||
topic.reload
|
||
expect(response.status).to eq(200)
|
||
expect(topic.slow_mode_seconds).to eq(3600)
|
||
end
|
||
end
|
||
|
||
context "when logged in as a regular user" do
|
||
it "does nothing if the user is not TL4" do
|
||
user.update!(trust_level: TrustLevel[3])
|
||
sign_in(user)
|
||
|
||
put "/t/#{topic.id}/slow_mode.json", params: { seconds: "3600" }
|
||
|
||
expect(response.status).to eq(403)
|
||
end
|
||
|
||
it "allows TL4 users to set the slow mode interval" do
|
||
user.update!(trust_level: TrustLevel[4])
|
||
sign_in(user)
|
||
|
||
put "/t/#{topic.id}/slow_mode.json", params: { seconds: "3600" }
|
||
|
||
topic.reload
|
||
expect(response.status).to eq(200)
|
||
expect(topic.slow_mode_seconds).to eq(3600)
|
||
end
|
||
end
|
||
|
||
context "with auto-disable slow mode" do
|
||
before { sign_in(admin) }
|
||
|
||
let!(:timestamp) { 1.week.from_now.to_formatted_s(:iso8601) }
|
||
|
||
it "sets a topic timer to clear the slow mode automatically" do
|
||
put "/t/#{topic.id}/slow_mode.json", params: { seconds: "3600", enabled_until: timestamp }
|
||
|
||
created_timer = TopicTimer.find_by(topic: topic)
|
||
execute_at = created_timer.execute_at.to_formatted_s(:iso8601)
|
||
|
||
expect(execute_at).to eq(timestamp)
|
||
end
|
||
|
||
it "deletes the topic timer" do
|
||
put "/t/#{topic.id}/slow_mode.json", params: { seconds: "3600", enabled_until: timestamp }
|
||
|
||
put "/t/#{topic.id}/slow_mode.json", params: { seconds: "0", enabled_until: timestamp }
|
||
|
||
created_timer = TopicTimer.find_by(topic: topic)
|
||
|
||
expect(created_timer).to be_nil
|
||
end
|
||
|
||
it "updates the existing timer" do
|
||
put "/t/#{topic.id}/slow_mode.json", params: { seconds: "3600", enabled_until: timestamp }
|
||
|
||
updated_timestamp = 1.hour.from_now.to_formatted_s(:iso8601)
|
||
|
||
put "/t/#{topic.id}/slow_mode.json",
|
||
params: {
|
||
seconds: "3600",
|
||
enabled_until: updated_timestamp,
|
||
}
|
||
|
||
created_timer = TopicTimer.find_by(topic: topic)
|
||
execute_at = created_timer.execute_at.to_formatted_s(:iso8601)
|
||
|
||
expect(execute_at).to eq(updated_timestamp)
|
||
end
|
||
end
|
||
|
||
describe "changes slow mode" do
|
||
before { sign_in(admin) }
|
||
|
||
it "should create a staff log entry" do
|
||
put "/t/#{topic.id}/slow_mode.json", params: { seconds: "3600" }
|
||
|
||
log = UserHistory.last
|
||
expect(log.acting_user_id).to eq(admin.id)
|
||
expect(log.topic_id).to eq(topic.id)
|
||
expect(log.action).to eq(UserHistory.actions[:topic_slow_mode_set])
|
||
|
||
put "/t/#{topic.id}/slow_mode.json", params: { seconds: "0" }
|
||
|
||
log = UserHistory.last
|
||
expect(log.acting_user_id).to eq(admin.id)
|
||
expect(log.topic_id).to eq(topic.id)
|
||
expect(log.action).to eq(UserHistory.actions[:topic_slow_mode_removed])
|
||
end
|
||
end
|
||
end
|
||
|
||
describe "#invite" do
|
||
context "when not logged in" do
|
||
it "should return the right response" do
|
||
post "/t/#{topic.id}/invite.json", params: { email: "jake@adventuretime.ooo" }
|
||
|
||
expect(response.status).to eq(403)
|
||
end
|
||
end
|
||
|
||
context "when logged in" do
|
||
before { sign_in(user) }
|
||
|
||
context "when topic id is not PM" do
|
||
fab!(:user_topic) { Fabricate(:topic, user: user) }
|
||
|
||
it "should return the right response" do
|
||
user.update!(trust_level: TrustLevel[2])
|
||
|
||
post "/t/#{user_topic.id}/invite.json", params: { email: "someguy@email.com" }
|
||
|
||
expect(response.status).to eq(422)
|
||
end
|
||
end
|
||
|
||
context "when topic id is invalid" do
|
||
it "should return the right response" do
|
||
id = topic.id
|
||
topic.destroy!
|
||
post "/t/#{id}/invite.json", params: { email: user.email }
|
||
|
||
expect(response.status).to eq(404)
|
||
end
|
||
end
|
||
|
||
it "requires an email parameter" do
|
||
post "/t/#{topic.id}/invite.json"
|
||
expect(response.status).to eq(422)
|
||
end
|
||
|
||
context "when PM has reached maximum allowed numbers of recipients" do
|
||
fab!(:pm) { Fabricate(:private_message_topic, user: user) }
|
||
|
||
fab!(:moderator_pm) { Fabricate(:private_message_topic, user: moderator) }
|
||
|
||
before { SiteSetting.max_allowed_message_recipients = 2 }
|
||
|
||
it "doesn't allow normal users to invite" do
|
||
post "/t/#{pm.id}/invite.json", params: { user: user_2.username }
|
||
expect(response.status).to eq(422)
|
||
expect(response.parsed_body["errors"]).to contain_exactly(
|
||
I18n.t(
|
||
"pm_reached_recipients_limit",
|
||
recipients_limit: SiteSetting.max_allowed_message_recipients,
|
||
),
|
||
)
|
||
end
|
||
|
||
it "allows staff to bypass limits" do
|
||
sign_in(moderator)
|
||
post "/t/#{moderator_pm.id}/invite.json", params: { user: user_2.username }
|
||
expect(response.status).to eq(200)
|
||
expect(moderator_pm.reload.topic_allowed_users.count).to eq(3)
|
||
end
|
||
end
|
||
|
||
it "does not disclose an existing user from an email invite" do
|
||
pm = Fabricate(:private_message_topic, user: user)
|
||
|
||
post "/t/#{pm.id}/invite.json", params: { email: user_2.email }
|
||
|
||
expect(response.status).to eq(422)
|
||
expect(response.parsed_body["failed"]).to eq("FAILED")
|
||
expect(pm.reload.topic_allowed_users.pluck(:user_id)).not_to include(user_2.id)
|
||
end
|
||
|
||
context "when user does not have permission to invite to the topic" do
|
||
fab!(:topic) { pm }
|
||
|
||
it "should return the right response" do
|
||
post "/t/#{topic.id}/invite.json", params: { user: user.username }
|
||
|
||
expect(response.status).to eq(403)
|
||
end
|
||
end
|
||
end
|
||
end
|
||
|
||
describe "#invite_group" do
|
||
let!(:admins) { Group[:admins] }
|
||
|
||
def invite_group(topic, expected_status)
|
||
post "/t/#{topic.id}/invite-group.json", params: { group: admins.name }
|
||
expect(response.status).to eq(expected_status)
|
||
end
|
||
|
||
before { admins.update!(messageable_level: Group::ALIAS_LEVELS[:everyone]) }
|
||
|
||
context "as an anon user" do
|
||
it "should be forbidden" do
|
||
invite_group(pm, 403)
|
||
end
|
||
end
|
||
|
||
context "as a normal user" do
|
||
before { sign_in(user) }
|
||
|
||
context "when user does not have permission to view the topic" do
|
||
it "should be forbidden" do
|
||
invite_group(pm, 403)
|
||
end
|
||
end
|
||
|
||
context "when user has permission to view the topic" do
|
||
before { pm.allowed_users << user }
|
||
|
||
it "should allow user to invite group to topic" do
|
||
invite_group(pm, 200)
|
||
expect(pm.allowed_groups.first.id).to eq(admins.id)
|
||
end
|
||
end
|
||
end
|
||
|
||
context "as an admin user" do
|
||
before { sign_in(admin) }
|
||
|
||
it "disallows inviting a group to a topic" do
|
||
invite_group(topic, 422)
|
||
end
|
||
|
||
it "allows inviting a group to a PM" do
|
||
invite_group(pm, 200)
|
||
expect(pm.allowed_groups.first.id).to eq(admins.id)
|
||
end
|
||
|
||
it "sends a notification to the group" do
|
||
user = Fabricate(:user)
|
||
Fabricate(:post, topic: pm)
|
||
admins.add(user)
|
||
admins
|
||
.group_users
|
||
.find_by(user_id: user.id)
|
||
.update!(notification_level: NotificationLevels.all[:watching])
|
||
|
||
Notification.delete_all
|
||
Jobs.run_immediately!
|
||
post "/t/#{pm.id}/invite-group.json", params: { group: "admins" }
|
||
|
||
expect(response.status).to eq(200)
|
||
expect(Notification.count).to be > 0
|
||
end
|
||
|
||
it "allows disabling notifications for that invite" do
|
||
user = Fabricate(:user)
|
||
Fabricate(:post, topic: pm)
|
||
admins.add(user)
|
||
admins
|
||
.group_users
|
||
.find_by(user_id: user.id)
|
||
.update!(notification_level: NotificationLevels.all[:watching])
|
||
|
||
Notification.delete_all
|
||
Jobs.run_immediately!
|
||
post "/t/#{pm.id}/invite-group.json", params: { group: "admins", should_notify: false }
|
||
|
||
expect(response.status).to eq(200)
|
||
expect(Notification.count).to eq(0)
|
||
end
|
||
end
|
||
|
||
context "when PM has reached maximum allowed numbers of recipients" do
|
||
fab!(:group) { Fabricate(:group, messageable_level: 99) }
|
||
fab!(:pm) { Fabricate(:private_message_topic, user: user) }
|
||
|
||
fab!(:moderator_pm) { Fabricate(:private_message_topic, user: moderator) }
|
||
|
||
before { SiteSetting.max_allowed_message_recipients = 2 }
|
||
|
||
it "doesn't allow normal users to invite" do
|
||
post "/t/#{pm.id}/invite-group.json", params: { group: group.name }
|
||
expect(response.status).to eq(422)
|
||
expect(response.parsed_body["errors"]).to contain_exactly(
|
||
I18n.t(
|
||
"pm_reached_recipients_limit",
|
||
recipients_limit: SiteSetting.max_allowed_message_recipients,
|
||
),
|
||
)
|
||
end
|
||
|
||
it "allows staff to bypass limits" do
|
||
sign_in(moderator)
|
||
post "/t/#{moderator_pm.id}/invite-group.json", params: { group: group.name }
|
||
expect(response.status).to eq(200)
|
||
expect(
|
||
moderator_pm.reload.topic_allowed_users.count + moderator_pm.topic_allowed_groups.count,
|
||
).to eq(3)
|
||
end
|
||
end
|
||
end
|
||
|
||
describe "shared drafts" do
|
||
before { SiteSetting.shared_drafts_category = shared_drafts_category.id }
|
||
|
||
describe "#update_shared_draft" do
|
||
fab!(:other_cat, :category)
|
||
fab!(:topic) { Fabricate(:topic, category: shared_drafts_category, visible: false) }
|
||
|
||
context "when anonymous" do
|
||
it "doesn't allow staff to update the shared draft" do
|
||
put "/t/#{topic.id}/shared-draft.json", params: { category_id: other_cat.id }
|
||
expect(response.code.to_i).to eq(403)
|
||
end
|
||
end
|
||
|
||
context "as a moderator" do
|
||
before { sign_in(moderator) }
|
||
|
||
context "with a shared draft" do
|
||
fab!(:shared_draft) { Fabricate(:shared_draft, topic: topic, category: category) }
|
||
it "allows staff to update the category id" do
|
||
put "/t/#{topic.id}/shared-draft.json", params: { category_id: other_cat.id }
|
||
expect(response.status).to eq(200)
|
||
topic.reload
|
||
expect(topic.shared_draft.category_id).to eq(other_cat.id)
|
||
end
|
||
end
|
||
|
||
context "without a shared draft" do
|
||
it "allows staff to update the category id" do
|
||
put "/t/#{topic.id}/shared-draft.json", params: { category_id: other_cat.id }
|
||
expect(response.status).to eq(200)
|
||
topic.reload
|
||
expect(topic.shared_draft.category_id).to eq(other_cat.id)
|
||
end
|
||
end
|
||
end
|
||
end
|
||
|
||
describe "#publish" do
|
||
fab!(:topic) { Fabricate(:topic, category: shared_drafts_category, visible: false) }
|
||
fab!(:post) { Fabricate(:post, user: post_author1, topic: topic) }
|
||
|
||
it "fails for anonymous users" do
|
||
put "/t/#{topic.id}/publish.json", params: { destination_category_id: category.id }
|
||
expect(response.status).to eq(403)
|
||
end
|
||
|
||
it "fails as a regular user" do
|
||
sign_in(user)
|
||
put "/t/#{topic.id}/publish.json", params: { destination_category_id: category.id }
|
||
expect(response.status).to eq(403)
|
||
end
|
||
|
||
context "as staff" do
|
||
before { sign_in(moderator) }
|
||
|
||
it "will publish the topic" do
|
||
put "/t/#{topic.id}/publish.json", params: { destination_category_id: category.id }
|
||
expect(response.status).to eq(200)
|
||
json = response.parsed_body["basic_topic"]
|
||
|
||
result = Topic.find(json["id"])
|
||
expect(result.category_id).to eq(category.id)
|
||
expect(result.visible).to eq(true)
|
||
end
|
||
|
||
it "fails if the destination category is the shared drafts category" do
|
||
put "/t/#{topic.id}/publish.json",
|
||
params: {
|
||
destination_category_id: shared_drafts_category.id,
|
||
}
|
||
expect(response.status).to eq(400)
|
||
end
|
||
end
|
||
end
|
||
end
|
||
|
||
describe "crawler" do
|
||
context "when not a crawler" do
|
||
it "renders with the application layout" do
|
||
get topic.relative_url
|
||
|
||
body = response.body
|
||
|
||
expect(body).to have_tag(:script, with: { "data-discourse-entrypoint" => "discourse" })
|
||
expect(body).to have_tag(:meta, with: { name: "fragment" })
|
||
end
|
||
|
||
it "renders the excerpt in the meta description decoded exactly once and tag-free" do
|
||
topic.update!(
|
||
excerpt:
|
||
%(Tom & Jerry's <span class="hashtag-icon-placeholder"></span>tale… "><script>alert(1)</script>),
|
||
)
|
||
|
||
get topic.relative_url
|
||
|
||
expect(response.body).not_to include("<script>alert(1)</script>")
|
||
meta = Nokogiri::HTML5.parse(response.body).at("meta[name='description']")
|
||
expect(meta["content"]).to eq(%(Tom & Jerry's tale… ">alert(1)))
|
||
end
|
||
end
|
||
|
||
context "with a tagged personal message rendered in the crawler layout" do
|
||
fab!(:participant) { Fabricate(:user, refresh_auto_groups: true) }
|
||
fab!(:pm_tag, :tag)
|
||
fab!(:pm) { Fabricate(:private_message_topic, user: user, recipient: participant) }
|
||
fab!(:pm_post) { Fabricate(:post, topic: pm, user: user) }
|
||
|
||
before do
|
||
SiteSetting.tagging_enabled = true
|
||
pm.tags << pm_tag
|
||
end
|
||
|
||
it "does not include the tags in the print view for participants who cannot tag PMs" do
|
||
sign_in(participant)
|
||
|
||
get "#{pm.relative_url}/print"
|
||
|
||
expect(response.status).to eq(200)
|
||
expect(response.body).not_to include(pm_tag.name)
|
||
end
|
||
|
||
it "includes the tags in the print view for participants who can tag PMs" do
|
||
SiteSetting.pm_tags_allowed_for_groups = Group::AUTO_GROUPS[:trust_level_0]
|
||
sign_in(participant)
|
||
|
||
get "#{pm.relative_url}/print"
|
||
|
||
expect(response.status).to eq(200)
|
||
expect(response.body).to include(pm_tag.name)
|
||
end
|
||
end
|
||
|
||
context "when a crawler" do
|
||
fab!(:page1_time) { 3.months.ago }
|
||
fab!(:page2_time) { 2.months.ago }
|
||
fab!(:page3_time) { 1.month.ago }
|
||
|
||
fab!(:page_1_posts) do
|
||
Fabricate.times(
|
||
20,
|
||
:post,
|
||
user: post_author2,
|
||
topic: topic,
|
||
created_at: page1_time,
|
||
updated_at: page1_time,
|
||
)
|
||
end
|
||
|
||
fab!(:page_2_posts) do
|
||
Fabricate.times(
|
||
20,
|
||
:post,
|
||
user: post_author3,
|
||
topic: topic,
|
||
created_at: page2_time,
|
||
updated_at: page2_time,
|
||
)
|
||
end
|
||
|
||
fab!(:page_3_posts) do
|
||
Fabricate.times(
|
||
2,
|
||
:post,
|
||
user: post_author3,
|
||
topic: topic,
|
||
created_at: page3_time,
|
||
updated_at: page3_time,
|
||
)
|
||
end
|
||
|
||
let!(:bot_user_agent) do
|
||
"Mozilla/5.0 (compatible; Googlebot/2.1; +http://www.google.com/bot.html)"
|
||
end
|
||
|
||
it "renders with the crawler layout, and handles proper pagination" do
|
||
get topic.relative_url, env: { "HTTP_USER_AGENT" => bot_user_agent }
|
||
|
||
body = response.body
|
||
|
||
expect(body).to have_tag(:body, with: { class: "crawler" })
|
||
expect(body).to_not have_tag(:meta, with: { name: "fragment" })
|
||
expect(body).to include('<link rel="next" href="' + topic.relative_url + "?page=2")
|
||
|
||
expect(body).to include("id='post_1'")
|
||
expect(body).to include("id='post_2'")
|
||
|
||
expect(response.headers["Last-Modified"]).to eq(page1_time.httpdate)
|
||
|
||
get topic.relative_url + "?page=2", env: { "HTTP_USER_AGENT" => bot_user_agent }
|
||
body = response.body
|
||
|
||
expect(response.headers["Last-Modified"]).to eq(page2_time.httpdate)
|
||
|
||
expect(body).to include("id='post_21'")
|
||
expect(body).to include("id='post_22'")
|
||
|
||
expect(body).to include('<link rel="prev" href="' + topic.relative_url)
|
||
expect(body).to include('<link rel="next" href="' + topic.relative_url + "?page=3")
|
||
|
||
get topic.relative_url + "?page=3", env: { "HTTP_USER_AGENT" => bot_user_agent }
|
||
body = response.body
|
||
|
||
page_3_posts.each { |post| expect(body).to include(post.cooked) }
|
||
|
||
expect(response.headers["Last-Modified"]).to eq(page3_time.httpdate)
|
||
expect(body).to include('<link rel="prev" href="' + topic.relative_url + "?page=2")
|
||
end
|
||
|
||
it "escapes the excerpt exactly once in the schema.org text meta" do
|
||
topic.update!(excerpt: %(Tom & Jerry… "><script>alert(1)</script>))
|
||
|
||
get topic.relative_url + "?page=2", env: { "HTTP_USER_AGENT" => bot_user_agent }
|
||
|
||
expect(response.body).not_to include("<script>alert(1)</script>")
|
||
meta = Nokogiri::HTML5.parse(response.body).at("meta[itemprop='text']")
|
||
expect(meta["content"]).to eq(%(Tom & Jerry… ">alert(1)))
|
||
end
|
||
|
||
it "only renders one post for non-canonical post-specific URLs" do
|
||
get "#{topic.relative_url}/24"
|
||
expect(response.body).to have_tag("#post_24")
|
||
expect(response.body).not_to have_tag("#post_23")
|
||
expect(response.body).not_to have_tag("#post_25")
|
||
expect(response.body).not_to have_tag("a", with: { rel: "next" })
|
||
expect(response.body).not_to have_tag("a", with: { rel: "prev" })
|
||
expect(response.body).to have_tag(
|
||
"a",
|
||
text: I18n.t("show_post_in_topic"),
|
||
with: {
|
||
href: "#{topic.relative_url}?page=2#post_24",
|
||
},
|
||
)
|
||
end
|
||
|
||
it "includes top-level author metadata when the view does not include the OP naturally" do
|
||
get "#{topic.relative_url}/2"
|
||
expect(body).to have_tag(
|
||
"[itemtype='http://schema.org/DiscussionForumPosting'] > [itemprop='author']",
|
||
)
|
||
|
||
get "#{topic.relative_url}/27"
|
||
expect(body).to have_tag(
|
||
"[itemtype='http://schema.org/DiscussionForumPosting'] > [itemprop='author']",
|
||
)
|
||
|
||
get "#{topic.relative_url}?page=2"
|
||
expect(body).to have_tag(
|
||
"[itemtype='http://schema.org/DiscussionForumPosting'] > [itemprop='author']",
|
||
)
|
||
end
|
||
|
||
it "works even when the author has been deleted" do
|
||
topic.update!(user_id: nil)
|
||
|
||
get "#{topic.relative_url}/2"
|
||
end
|
||
|
||
it "adds breadcrumbs to the correct subcategory and category url in subfolder" do
|
||
set_subfolder "/subpath"
|
||
|
||
subcategory = Fabricate(:category, parent_category_id: category.id)
|
||
topic.update!(category: subcategory)
|
||
|
||
get "/t/#{topic.slug}/#{topic.id}",
|
||
env: {
|
||
"HTTP_USER_AGENT" => "Mozilla/5.0 ...",
|
||
"HTTP_VIA" => "HTTP/1.0 web.archive.org",
|
||
}
|
||
expect(response.body).to have_tag(
|
||
"a",
|
||
with: {
|
||
href: subcategory.url,
|
||
},
|
||
text: subcategory.name,
|
||
)
|
||
expect(response.body).to have_tag("a", with: { href: category.url }, text: category.name)
|
||
end
|
||
|
||
context "with canonical_url" do
|
||
fab!(:topic_embed) { Fabricate(:topic_embed, embed_url: "https://markvanlan.com") }
|
||
|
||
it "set to topic.url when embed_set_canonical_url is false" do
|
||
get topic_embed.topic.url, env: { "HTTP_USER_AGENT" => bot_user_agent }
|
||
expect(response.body).to include('<link rel="canonical" href="' + topic_embed.topic.url)
|
||
end
|
||
|
||
it "set to topic_embed.embed_url when embed_set_canonical_url is true" do
|
||
SiteSetting.embed_set_canonical_url = true
|
||
get topic_embed.topic.url, env: { "HTTP_USER_AGENT" => bot_user_agent }
|
||
expect(response.body).to include('<link rel="canonical" href="' + topic_embed.embed_url)
|
||
end
|
||
end
|
||
|
||
context "with wayback machine" do
|
||
it "renders crawler layout" do
|
||
get topic.relative_url,
|
||
env: {
|
||
"HTTP_USER_AGENT" =>
|
||
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_4) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/74.0.3729.169 Safari/537.36",
|
||
"HTTP_VIA" => "HTTP/1.0 web.archive.org (Wayback Save Page)",
|
||
}
|
||
body = response.body
|
||
|
||
expect(body).to have_tag(:body, with: { class: "crawler" })
|
||
expect(body).to_not have_tag(:meta, with: { name: "fragment" })
|
||
end
|
||
end
|
||
|
||
context "when content localization is enabled" do
|
||
fab!(:category) { Fabricate(:category, locale: "en") }
|
||
fab!(:subcategory) { Fabricate(:category, parent_category: category, locale: "en") }
|
||
fab!(:tag)
|
||
|
||
before do
|
||
SiteSetting.content_localization_enabled = true
|
||
SiteSetting.allow_user_locale = true
|
||
SiteSetting.set_locale_from_param = true
|
||
|
||
topic.update!(category: subcategory, tags: [tag], locale: "en")
|
||
topic.first_post.update(locale: "en")
|
||
# randomly create localizations for an untested locale
|
||
topic
|
||
.posts
|
||
.sample(3)
|
||
.each do |post|
|
||
post.update!(locale: "en")
|
||
Fabricate(:post_localization, post:, locale: "de")
|
||
end
|
||
end
|
||
|
||
describe "when tl param is absent" do
|
||
fab!(:pt_topic) { Fabricate(:topic_localization, topic:, locale: "pt") }
|
||
fab!(:pt_category) { Fabricate(:category_localization, category:, locale: "pt") }
|
||
fab!(:pt_subcategory) do
|
||
Fabricate(:category_localization, category: subcategory, locale: "pt")
|
||
end
|
||
fab!(:pt_first_post) do
|
||
Fabricate(:post_localization, post: topic.first_post, locale: "pt_BR")
|
||
end
|
||
|
||
it "localizes (english) topic for crawler to (portuguese) default locale when localization exists" do
|
||
SiteSetting.default_locale = "pt"
|
||
|
||
get topic.relative_url, env: { "HTTP_USER_AGENT" => bot_user_agent }
|
||
|
||
expect(response.body).to include(pt_topic.title)
|
||
expect(response.body).to include(pt_category.name)
|
||
expect(response.body).to include(pt_subcategory.name)
|
||
expect(response.body).to include(pt_first_post.cooked)
|
||
end
|
||
|
||
it "leaves topic as-is if no localization" do
|
||
SiteSetting.default_locale = "es"
|
||
|
||
get topic.relative_url, env: { "HTTP_USER_AGENT" => bot_user_agent }
|
||
|
||
expect(response.body).to include(topic.title)
|
||
expect(response.body).to include(category.name)
|
||
expect(response.body).to include(subcategory.name)
|
||
expect(response.body).to include(topic.first_post.cooked)
|
||
end
|
||
end
|
||
|
||
describe "when tl param is present ?tl=ja" do
|
||
fab!(:ja_topic) { Fabricate(:topic_localization, topic:, locale: "ja") }
|
||
fab!(:ja_category) { Fabricate(:category_localization, category:, locale: "ja") }
|
||
fab!(:ja_subcategory) do
|
||
Fabricate(:category_localization, category: subcategory, locale: "ja")
|
||
end
|
||
|
||
it "localizes topic for crawler" do
|
||
get topic.relative_url,
|
||
env: {
|
||
"HTTP_USER_AGENT" => bot_user_agent,
|
||
},
|
||
params: {
|
||
tl: "ja",
|
||
}
|
||
|
||
expect(response.body).to include(ja_topic.title)
|
||
# breadcrumbs
|
||
expect(response.body).to include(ja_category.name)
|
||
expect(response.body).to include(ja_subcategory.name)
|
||
end
|
||
|
||
it "does not persist localized fancy_title to the database when topic fancy_title is null" do
|
||
topic.update_column(:fancy_title, nil)
|
||
|
||
get topic.relative_url,
|
||
env: {
|
||
"HTTP_USER_AGENT" => bot_user_agent,
|
||
},
|
||
params: {
|
||
tl: "ja",
|
||
}
|
||
|
||
expect(response.status).to eq(200)
|
||
|
||
db_fancy_title = Topic.where(id: topic.id).pick(:fancy_title)
|
||
localized_fancy_title = Topic.fancy_title(ja_topic.title)
|
||
expect(db_fancy_title).not_to eq(localized_fancy_title)
|
||
end
|
||
end
|
||
|
||
it "does not have N+1s when loading localizations" do
|
||
Fabricate(:topic_localization, topic:, locale: "ja")
|
||
topic
|
||
.posts
|
||
.where("post_number < 4")
|
||
.each { |post| Fabricate(:post_localization, post:, locale: "ja") }
|
||
|
||
initial_sql_queries =
|
||
track_sql_queries do
|
||
get topic.relative_url,
|
||
env: {
|
||
"HTTP_USER_AGENT" => bot_user_agent,
|
||
},
|
||
params: {
|
||
tl: "ja",
|
||
}
|
||
expect(response.status).to eq(200)
|
||
end.select { |q| q.include?("_localizations") }.count
|
||
|
||
Fabricate(:post_localization, post: topic.posts.find_by_post_number(4), locale: "ja")
|
||
|
||
new_sql_queries =
|
||
track_sql_queries do
|
||
get topic.relative_url,
|
||
env: {
|
||
"HTTP_USER_AGENT" => bot_user_agent,
|
||
},
|
||
params: {
|
||
tl: "ja",
|
||
}
|
||
expect(response.status).to eq(200)
|
||
end.select { |q| q.include?("_localizations") }.count
|
||
|
||
expect(new_sql_queries).to eq(initial_sql_queries)
|
||
end
|
||
end
|
||
end
|
||
end
|
||
|
||
describe "#reset_bump_date" do
|
||
context "with errors" do
|
||
it "needs you to be logged in" do
|
||
put "/t/#{topic.id}/reset-bump-date.json"
|
||
expect(response.status).to eq(403)
|
||
end
|
||
|
||
[:user].each do |user|
|
||
it "denies access for #{user}" do
|
||
sign_in(Fabricate(user))
|
||
put "/t/#{topic.id}/reset-bump-date.json"
|
||
expect(response.status).to eq(403)
|
||
end
|
||
end
|
||
|
||
it "should fail for non-existent topic" do
|
||
max_id = Topic.maximum(:id)
|
||
sign_in(admin)
|
||
put "/t/#{max_id + 1}/reset-bump-date.json"
|
||
expect(response.status).to eq(404)
|
||
end
|
||
|
||
it "denies access for TL4 user on a topic in a restricted category" do
|
||
restricted_topic = Fabricate(:topic, category: staff_category)
|
||
sign_in(trust_level_4)
|
||
put "/t/#{restricted_topic.id}/reset-bump-date.json"
|
||
expect(response.status).to eq(403)
|
||
end
|
||
end
|
||
|
||
%i[admin moderator trust_level_4].each do |user|
|
||
it "should reset bumped_at as #{user}" do
|
||
sign_in(public_send(user))
|
||
topic.update!(bumped_at: 1.hour.ago)
|
||
timestamp = 1.day.ago
|
||
Fabricate(:post, user: post_author1, topic: topic, created_at: timestamp)
|
||
|
||
put "/t/#{topic.id}/reset-bump-date.json"
|
||
expect(response.status).to eq(200)
|
||
expect(topic.reload.bumped_at).to eq_time(timestamp)
|
||
end
|
||
end
|
||
|
||
context "with a post_id parameter" do
|
||
before { sign_in(admin) }
|
||
|
||
it "resets bump correctly" do
|
||
post1 = Fabricate(:post, user: post_author1, topic: topic, created_at: 2.days.ago)
|
||
_post2 = Fabricate(:post, user: post_author1, topic: topic, created_at: 1.day.ago)
|
||
|
||
put "/t/#{topic.id}/reset-bump-date/#{post1.id}.json"
|
||
expect(response.status).to eq(200)
|
||
expect(topic.reload.bumped_at).to eq_time(post1.created_at)
|
||
end
|
||
|
||
it "does not raise an error for an inexistent post" do
|
||
id = (SecureRandom.random_number * 100_000_000).to_i
|
||
original_bumped_at = topic.bumped_at
|
||
|
||
put "/t/#{topic.id}/reset-bump-date/#{id}.json"
|
||
expect(response.status).to eq(200)
|
||
expect(topic.reload.bumped_at).to eq_time(original_bumped_at)
|
||
end
|
||
end
|
||
end
|
||
|
||
describe "#private_message_reset_new" do
|
||
fab!(:group) do
|
||
Fabricate(:group, messageable_level: Group::ALIAS_LEVELS[:everyone]).tap { |g| g.add(user_2) }
|
||
end
|
||
|
||
fab!(:group_message) do
|
||
create_post(
|
||
user: user,
|
||
target_group_names: [group.name],
|
||
archetype: Archetype.private_message,
|
||
).topic
|
||
end
|
||
|
||
fab!(:private_message) do
|
||
create_post(
|
||
user: user,
|
||
target_usernames: [user_2.username],
|
||
archetype: Archetype.private_message,
|
||
).topic
|
||
end
|
||
|
||
fab!(:private_message_2) do
|
||
create_post(
|
||
user: user,
|
||
target_usernames: [user_2.username],
|
||
archetype: Archetype.private_message,
|
||
).topic
|
||
end
|
||
|
||
before { sign_in(user_2) }
|
||
|
||
it "returns the right response when inbox param is missing" do
|
||
put "/topics/pm-reset-new.json"
|
||
|
||
expect(response.status).to eq(400)
|
||
end
|
||
|
||
it "returns the right response when trying to reset new private messages of an invalid group" do
|
||
put "/topics/pm-reset-new.json", params: { inbox: "group", group_name: "randomgroup" }
|
||
|
||
expect(response.status).to eq(404)
|
||
end
|
||
|
||
it "returns the right response when trying to reset new private messages of a restricted group" do
|
||
sign_in(user)
|
||
|
||
put "/topics/pm-reset-new.json", params: { inbox: "group", group_name: group.name }
|
||
|
||
expect(response.status).to eq(404)
|
||
end
|
||
|
||
it "can reset all new group private messages" do
|
||
put "/topics/pm-reset-new.json", params: { inbox: "group", group_name: group.name }
|
||
|
||
expect(response.status).to eq(200)
|
||
expect(response.parsed_body["topic_ids"]).to contain_exactly(group_message.id)
|
||
|
||
expect(DismissedTopicUser.count).to eq(1)
|
||
|
||
expect(DismissedTopicUser.exists?(topic: group_message, user: user_2)).to eq(true)
|
||
end
|
||
|
||
it "can reset new personal private messages" do
|
||
put "/topics/pm-reset-new.json", params: { inbox: "user" }
|
||
|
||
expect(response.status).to eq(200)
|
||
expect(response.parsed_body["topic_ids"]).to contain_exactly(
|
||
private_message.id,
|
||
private_message_2.id,
|
||
)
|
||
|
||
expect(DismissedTopicUser.count).to eq(2)
|
||
|
||
expect(
|
||
DismissedTopicUser.exists?(user: user_2, topic: [private_message, private_message_2]),
|
||
).to eq(true)
|
||
end
|
||
|
||
it "can reset new personal and group private messages" do
|
||
stub_const(TopicQuery, "DEFAULT_PER_PAGE_COUNT", 1) do
|
||
put "/topics/pm-reset-new.json", params: { inbox: "all" }
|
||
|
||
expect(response.status).to eq(200)
|
||
|
||
expect(DismissedTopicUser.count).to eq(3)
|
||
|
||
expect(
|
||
DismissedTopicUser.exists?(
|
||
user: user_2,
|
||
topic: [private_message, private_message_2, group_message],
|
||
),
|
||
).to eq(true)
|
||
end
|
||
end
|
||
|
||
it "returns the right response is topic_ids params is not valid" do
|
||
put "/topics/pm-reset-new.json", params: { topic_ids: "1" }
|
||
|
||
expect(response.status).to eq(400)
|
||
end
|
||
|
||
it "can reset new private messages from given topic ids" do
|
||
put "/topics/pm-reset-new.json", params: { topic_ids: [group_message.id, "12345"] }
|
||
|
||
expect(response.status).to eq(200)
|
||
|
||
expect(DismissedTopicUser.count).to eq(1)
|
||
|
||
expect(DismissedTopicUser.exists?(topic: group_message, user: user_2)).to eq(true)
|
||
|
||
put "/topics/pm-reset-new.json", params: { topic_ids: [private_message.id, "12345"] }
|
||
|
||
expect(response.status).to eq(200)
|
||
|
||
expect(DismissedTopicUser.exists?(topic: private_message, user: user_2)).to eq(true)
|
||
end
|
||
end
|
||
|
||
describe "#archive_message" do
|
||
fab!(:group) do
|
||
Fabricate(:group, messageable_level: Group::ALIAS_LEVELS[:everyone]).tap { |g| g.add(user) }
|
||
end
|
||
|
||
fab!(:group_message) do
|
||
create_post(
|
||
user: user,
|
||
target_group_names: [group.name],
|
||
archetype: Archetype.private_message,
|
||
).topic
|
||
end
|
||
|
||
it "should be able to archive a private message" do
|
||
sign_in(user)
|
||
|
||
message =
|
||
MessageBus
|
||
.track_publish(PrivateMessageTopicTrackingState.group_channel(group.id)) do
|
||
put "/t/#{group_message.id}/archive-message.json"
|
||
|
||
expect(response.status).to eq(200)
|
||
end
|
||
.first
|
||
|
||
expect(message.data["message_type"]).to eq(
|
||
PrivateMessageTopicTrackingState::GROUP_ARCHIVE_MESSAGE_TYPE,
|
||
)
|
||
|
||
expect(message.data["payload"]["acting_user_id"]).to eq(user.id)
|
||
|
||
body = response.parsed_body
|
||
|
||
expect(body["group_name"]).to eq(group.name)
|
||
end
|
||
|
||
it "returns not found for a user who is not a participant of the message" do
|
||
sign_in(user_2)
|
||
|
||
put "/t/#{group_message.id}/archive-message.json"
|
||
expect(response.status).to eq(404)
|
||
expect(response.parsed_body["error_type"]).to eq("not_found")
|
||
end
|
||
end
|
||
|
||
describe "#move_to_inbox" do
|
||
fab!(:group) do
|
||
Fabricate(:group, messageable_level: Group::ALIAS_LEVELS[:everyone]).tap { |g| g.add(user) }
|
||
end
|
||
|
||
fab!(:group_message) do
|
||
create_post(
|
||
user: user,
|
||
target_group_names: [group.name],
|
||
archetype: Archetype.private_message,
|
||
).topic
|
||
end
|
||
|
||
it "returns not found for a user who is not a participant of the message" do
|
||
sign_in(user_2)
|
||
|
||
put "/t/#{group_message.id}/move-to-inbox.json"
|
||
expect(response.status).to eq(404)
|
||
expect(response.parsed_body["error_type"]).to eq("not_found")
|
||
end
|
||
end
|
||
|
||
describe "#set_notifications" do
|
||
let(:watching) { NotificationLevels.topic_levels[:watching] }
|
||
|
||
it "rejects `username` param for session requests" do
|
||
sign_in(user)
|
||
post "/t/#{topic.id}/notifications.json",
|
||
params: {
|
||
username: user_2.username,
|
||
notification_level: watching,
|
||
}
|
||
|
||
expect(response.status).to eq(403)
|
||
expect(TopicUser.find_by(user: user, topic: topic)).to be_blank
|
||
expect(TopicUser.find_by(user: user_2, topic: topic)).to be_blank
|
||
end
|
||
|
||
describe "via API" do
|
||
it "admin can target another user with `username` param" do
|
||
api_key = Fabricate(:api_key, user: admin).key
|
||
post "/t/#{topic.id}/notifications",
|
||
params: {
|
||
username: user.username,
|
||
notification_level: watching,
|
||
},
|
||
headers: {
|
||
HTTP_API_KEY: api_key,
|
||
HTTP_API_USERNAME: admin.username,
|
||
}
|
||
|
||
expect(TopicUser.find_by(user: user, topic: topic).notification_level).to eq(watching)
|
||
end
|
||
|
||
it "admin acts on self when `username` param is absent" do
|
||
api_key = Fabricate(:api_key, user: admin).key
|
||
post "/t/#{topic.id}/notifications",
|
||
params: {
|
||
notification_level: watching,
|
||
},
|
||
headers: {
|
||
HTTP_API_KEY: api_key,
|
||
HTTP_API_USERNAME: admin.username,
|
||
}
|
||
|
||
expect(TopicUser.find_by(user: admin, topic: topic).notification_level).to eq(watching)
|
||
end
|
||
|
||
it "non-admin gets 403 when passing `username` to target another user" do
|
||
api_key = Fabricate(:api_key, user: user).key
|
||
post "/t/#{topic.id}/notifications",
|
||
params: {
|
||
username: user_2.username,
|
||
notification_level: watching,
|
||
},
|
||
headers: {
|
||
HTTP_API_KEY: api_key,
|
||
HTTP_API_USERNAME: user.username,
|
||
}
|
||
|
||
expect(response.status).to eq(403)
|
||
expect(TopicUser.find_by(user: user, topic: topic)).to be_blank
|
||
expect(TopicUser.find_by(user: user_2, topic: topic)).to be_blank
|
||
end
|
||
|
||
it "non-admin acts on self when `username` param is absent" do
|
||
api_key = Fabricate(:api_key, user: user).key
|
||
post "/t/#{topic.id}/notifications",
|
||
params: {
|
||
notification_level: watching,
|
||
},
|
||
headers: {
|
||
HTTP_API_KEY: api_key,
|
||
HTTP_API_USERNAME: user.username,
|
||
}
|
||
|
||
expect(TopicUser.find_by(user: user, topic: topic).notification_level).to eq(watching)
|
||
end
|
||
end
|
||
|
||
it "returns not found when a regular user sets notifications on a private message they cannot see" do
|
||
sign_in(user)
|
||
|
||
post "/t/#{pm.id}/notifications.json",
|
||
params: {
|
||
notification_level: NotificationLevels.topic_levels[:watching],
|
||
}
|
||
|
||
expect(response.status).to eq(404)
|
||
expect(response.parsed_body["error_type"]).to eq("not_found")
|
||
expect(TopicUser.find_by(user: user, topic: pm)).to be_blank
|
||
end
|
||
|
||
it "returns not found when a regular user sets notifications on a topic in a restricted category" do
|
||
restricted_topic = Fabricate(:topic, category: staff_category)
|
||
sign_in(user)
|
||
|
||
post "/t/#{restricted_topic.id}/notifications.json",
|
||
params: {
|
||
notification_level: NotificationLevels.topic_levels[:watching],
|
||
}
|
||
|
||
expect(response.status).to eq(404)
|
||
expect(response.parsed_body["error_type"]).to eq("not_found")
|
||
expect(TopicUser.find_by(user: user, topic: restricted_topic)).to be_blank
|
||
end
|
||
end
|
||
|
||
describe ".defer_topic_view" do
|
||
fab!(:topic)
|
||
fab!(:user)
|
||
|
||
before { Jobs.run_immediately! }
|
||
|
||
it "does nothing if topic does not exist" do
|
||
topic.destroy!
|
||
expect {
|
||
Scheduler::Defer.capture_later do
|
||
TopicsController.defer_topic_view(topic.id, "1.2.3.4", user.id)
|
||
end
|
||
}.not_to change { TopicViewItem.count }
|
||
end
|
||
|
||
it "does nothing if user from ID does not exist" do
|
||
user.destroy!
|
||
expect {
|
||
Scheduler::Defer.capture_later do
|
||
TopicsController.defer_topic_view(topic.id, "1.2.3.4", user.id)
|
||
end
|
||
}.not_to change { TopicViewItem.count }
|
||
end
|
||
|
||
it "does nothing if the topic is a shared draft" do
|
||
topic.shared_draft = Fabricate(:shared_draft)
|
||
|
||
expect {
|
||
Scheduler::Defer.capture_later do
|
||
TopicsController.defer_topic_view(topic.id, "1.2.3.4", user.id)
|
||
end
|
||
}.not_to change { TopicViewItem.count }
|
||
end
|
||
|
||
it "does nothing if user cannot see topic" do
|
||
topic.update!(category: Fabricate(:private_category, group: Fabricate(:group)))
|
||
|
||
expect {
|
||
Scheduler::Defer.capture_later do
|
||
TopicsController.defer_topic_view(topic.id, "1.2.3.4", user.id)
|
||
end
|
||
}.not_to change { TopicViewItem.count }
|
||
end
|
||
|
||
it "creates a topic view" do
|
||
expect {
|
||
Scheduler::Defer.capture_later do
|
||
TopicsController.defer_topic_view(topic.id, "1.2.3.4", user.id)
|
||
end
|
||
}.to change { TopicViewItem.count }
|
||
end
|
||
end
|
||
|
||
describe "allow_embed_mode" do
|
||
fab!(:topic)
|
||
|
||
before { SiteSetting.embed_full_app = true }
|
||
|
||
it "keeps X-Frame-Options when embed_mode param is missing" do
|
||
get("/t/#{topic.slug}/#{topic.id}")
|
||
expect(response.headers["X-Frame-Options"]).to eq("SAMEORIGIN")
|
||
end
|
||
|
||
it "keeps X-Frame-Options when embed_mode is present but referer is invalid" do
|
||
get("/t/#{topic.slug}/#{topic.id}", params: { embed_mode: "true" })
|
||
expect(response.headers["X-Frame-Options"]).to eq("SAMEORIGIN")
|
||
end
|
||
|
||
it "strips X-Frame-Options when embed_mode is present and referer matches embeddable host" do
|
||
Fabricate(:embeddable_host, host: "example.com")
|
||
get(
|
||
"/t/#{topic.slug}/#{topic.id}",
|
||
params: {
|
||
embed_mode: "true",
|
||
},
|
||
headers: {
|
||
"HTTP_REFERER" => "https://example.com/page",
|
||
},
|
||
)
|
||
expect(response.headers).not_to include("X-Frame-Options")
|
||
end
|
||
|
||
it "strips X-Frame-Options when embed_mode is present and embed_any_origin is enabled" do
|
||
SiteSetting.embed_any_origin = true
|
||
get("/t/#{topic.slug}/#{topic.id}", params: { embed_mode: "true" })
|
||
expect(response.headers).not_to include("X-Frame-Options")
|
||
end
|
||
|
||
it "keeps X-Frame-Options when embed_full_app is disabled" do
|
||
SiteSetting.embed_full_app = false
|
||
Fabricate(:embeddable_host, host: "example.com")
|
||
get(
|
||
"/t/#{topic.slug}/#{topic.id}",
|
||
params: {
|
||
embed_mode: "true",
|
||
},
|
||
headers: {
|
||
"HTTP_REFERER" => "https://example.com/page",
|
||
},
|
||
)
|
||
expect(response.headers["X-Frame-Options"]).to eq("SAMEORIGIN")
|
||
end
|
||
|
||
it "applies class_name to the html element when embed_mode is allowed" do
|
||
SiteSetting.embed_any_origin = true
|
||
get("/t/#{topic.slug}/#{topic.id}", params: { embed_mode: "true", class_name: "lee-af" })
|
||
expect(response.body).to match(/<html[^>]*\bclass="[^"]*\blee-af\b/)
|
||
end
|
||
|
||
it "ignores class_name when embed_mode is not allowed" do
|
||
get("/t/#{topic.slug}/#{topic.id}", params: { class_name: "lee-af" })
|
||
expect(response.body).not_to match(/<html[^>]*\blee-af\b/)
|
||
end
|
||
|
||
it "ignores class_name with invalid characters" do
|
||
SiteSetting.embed_any_origin = true
|
||
get(
|
||
"/t/#{topic.slug}/#{topic.id}",
|
||
params: {
|
||
embed_mode: "true",
|
||
class_name: "lee\" onload=alert(1)",
|
||
},
|
||
)
|
||
expect(response.body).not_to include("onload=alert(1)")
|
||
end
|
||
end
|
||
|
||
describe "third-party analytics in embed mode" do
|
||
fab!(:topic)
|
||
|
||
before do
|
||
SiteSetting.embed_full_app = true
|
||
SiteSetting.gtm_container_id = "GTM-ABCDEF"
|
||
SiteSetting.adobe_analytics_tags_url = "https://assets.adobedtm.com/launch-EN.min.js"
|
||
end
|
||
|
||
def parsed_body
|
||
Nokogiri::HTML5.fragment(response.body)
|
||
end
|
||
|
||
it "renders analytics tags by default" do
|
||
get "/t/#{topic.slug}/#{topic.id}"
|
||
expect(parsed_body.css("#data-google-tag-manager")).to be_present
|
||
expect(
|
||
parsed_body.css("script[src='https://assets.adobedtm.com/launch-EN.min.js']"),
|
||
).to be_present
|
||
end
|
||
|
||
it "skips analytics tags when loaded in embed mode" do
|
||
get "/t/#{topic.slug}/#{topic.id}", params: { embed_mode: "true" }
|
||
expect(parsed_body.css("#data-google-tag-manager")).to be_empty
|
||
expect(
|
||
parsed_body.css("script[src='https://assets.adobedtm.com/launch-EN.min.js']"),
|
||
).to be_empty
|
||
end
|
||
|
||
it "still renders analytics tags in embed mode when suppression setting is disabled" do
|
||
SiteSetting.suppress_third_party_analytics_in_embed = false
|
||
get "/t/#{topic.slug}/#{topic.id}", params: { embed_mode: "true" }
|
||
expect(parsed_body.css("#data-google-tag-manager")).to be_present
|
||
expect(
|
||
parsed_body.css("script[src='https://assets.adobedtm.com/launch-EN.min.js']"),
|
||
).to be_present
|
||
end
|
||
end
|
||
end
|