mirror of
https://gh.wpcy.net/https://github.com/discourse/discourse.git
synced 2026-05-04 17:02:35 +08:00
**Previously**, a post's `reply_to_post_number` was fixed at creation time and couldn't be changed, so users who accidentally replied to the wrong post had no way to correct the reply relationship. **In this update**, the edit composer lets users with edit permission change or remove the post a reply points to; `PostRevisor` validates the new target, keeps `reply_to_user_id` and the parent's `reply_count` in sync, and records the change in post revisions. https://github.com/user-attachments/assets/1741179c-45e1-4404-88d0-0c97ff0c7969 --------- Co-authored-by: Mark VanLandingham <markvanlan@gmail.com>
2285 lines
77 KiB
Ruby
2285 lines
77 KiB
Ruby
# frozen_string_literal: true
|
|
|
|
describe PostRevisor do
|
|
fab!(:topic)
|
|
fab!(:newuser) { Fabricate(:newuser, last_seen_at: Date.today) }
|
|
fab!(:user) { Fabricate(:user, refresh_auto_groups: true) }
|
|
fab!(:coding_horror)
|
|
fab!(:admin) { Fabricate(:admin, refresh_auto_groups: true) }
|
|
fab!(:moderator)
|
|
let(:post_args) { { user: newuser, topic: topic } }
|
|
|
|
describe "TopicChanges" do
|
|
let(:tc) do
|
|
topic.reload
|
|
PostRevisor::TopicChanges.new(topic, topic.user)
|
|
end
|
|
|
|
it "provides a guardian" do
|
|
expect(tc.guardian).to be_an_instance_of Guardian
|
|
end
|
|
|
|
it "tracks changes properly" do
|
|
expect(tc.diff).to eq({})
|
|
|
|
# it remembers changes we tell it to
|
|
tc.record_change("height", "180cm", "170cm")
|
|
expect(tc.diff["height"]).to eq(%w[180cm 170cm])
|
|
|
|
# it works with arrays of values
|
|
tc.record_change("colors", nil, %w[red blue])
|
|
expect(tc.diff["colors"]).to eq([nil, %w[red blue]])
|
|
|
|
# it does not record changes to the same val
|
|
tc.record_change("wat", "js", "js")
|
|
expect(tc.diff["wat"]).to be_nil
|
|
|
|
tc.record_change("tags", %w[a b], %w[a b])
|
|
expect(tc.diff["tags"]).to be_nil
|
|
end
|
|
end
|
|
|
|
describe "editing category" do
|
|
it "triggers the :post_edited event with topic_changed?" do
|
|
category = Fabricate(:category)
|
|
category.set_permissions(everyone: :full)
|
|
category.save!
|
|
post = create_post
|
|
events = DiscourseEvent.track_events { post.revise(post.user, category_id: category.id) }
|
|
|
|
event = events.find { |e| e[:event_name] == :post_edited }
|
|
|
|
expect(event[:params].first).to eq(post)
|
|
expect(event[:params].second).to eq(true)
|
|
expect(event[:params].third).to be_kind_of(PostRevisor)
|
|
expect(event[:params].third.topic_diff).to eq(
|
|
{ "category_id" => [SiteSetting.uncategorized_category_id, category.id] },
|
|
)
|
|
end
|
|
|
|
it "does not revise category when no permission to create a topic in category" do
|
|
category = Fabricate(:category)
|
|
category.set_permissions(staff: :full)
|
|
category.save!
|
|
|
|
post = create_post
|
|
old_id = post.topic.category_id
|
|
|
|
post.revise(post.user, category_id: category.id)
|
|
|
|
post.reload
|
|
expect(post.topic.category_id).to eq(old_id)
|
|
|
|
category.set_permissions(everyone: :full)
|
|
category.save!
|
|
|
|
post.revise(post.user, category_id: category.id)
|
|
|
|
post.reload
|
|
expect(post.topic.category_id).to eq(category.id)
|
|
end
|
|
|
|
it "does not revise category when the destination category requires topic approval" do
|
|
new_category = Fabricate(:category)
|
|
new_category.require_topic_approval = true
|
|
new_category.save!
|
|
|
|
post = create_post
|
|
old_category_id = post.topic.category_id
|
|
|
|
post.revise(post.user, category_id: new_category.id)
|
|
expect(post.reload.topic.category_id).to eq(old_category_id)
|
|
|
|
new_category.require_topic_approval = false
|
|
new_category.save!
|
|
|
|
post.revise(post.user, category_id: new_category.id)
|
|
expect(post.reload.topic.category_id).to eq(new_category.id)
|
|
end
|
|
|
|
it "does not revise category if incorrect amount of tags" do
|
|
SiteSetting.create_tag_allowed_groups = Group::AUTO_GROUPS[:trust_level_0]
|
|
SiteSetting.tag_topic_allowed_groups = Group::AUTO_GROUPS[:trust_level_0]
|
|
|
|
new_category = Fabricate(:category, minimum_required_tags: 1)
|
|
|
|
post = create_post
|
|
old_category_id = post.topic.category_id
|
|
|
|
post.revise(post.user, category_id: new_category.id)
|
|
expect(post.reload.topic.category_id).to eq(old_category_id)
|
|
|
|
tag = Fabricate(:tag)
|
|
topic_tag = Fabricate(:topic_tag, topic: post.topic, tag: tag)
|
|
post.revise(post.user, category_id: new_category.id)
|
|
expect(post.reload.topic.category_id).to eq(new_category.id)
|
|
topic_tag.destroy
|
|
|
|
post.revise(post.user, category_id: new_category.id, tags: ["test_tag"])
|
|
expect(post.reload.topic.category_id).to eq(new_category.id)
|
|
end
|
|
|
|
it "allows category change with localized tags" do
|
|
SiteSetting.create_tag_allowed_groups = Group::AUTO_GROUPS[:trust_level_0]
|
|
SiteSetting.tag_topic_allowed_groups = Group::AUTO_GROUPS[:trust_level_0]
|
|
|
|
tag = Fabricate(:tag)
|
|
tag_group = Fabricate(:tag_group, tags: [tag])
|
|
old_category = Fabricate(:category)
|
|
new_category = Fabricate(:category, tag_groups: [tag_group])
|
|
|
|
post = create_post(category: old_category)
|
|
|
|
post.revise(post.user, category_id: new_category.id, tags: [{ id: tag.id, name: tag.name }])
|
|
expect(post.reload.topic.category_id).to eq(new_category.id)
|
|
end
|
|
|
|
it "allows category change when clearing all tags with an empty array" do
|
|
SiteSetting.create_tag_allowed_groups = Group::AUTO_GROUPS[:trust_level_0]
|
|
SiteSetting.tag_topic_allowed_groups = Group::AUTO_GROUPS[:trust_level_0]
|
|
|
|
tag = Fabricate(:tag)
|
|
old_category = Fabricate(:category)
|
|
new_category = Fabricate(:category)
|
|
|
|
post = create_post(category: old_category, tags: [tag.name])
|
|
expect(post.topic.tags).to contain_exactly(tag)
|
|
|
|
post.revise(post.user, category_id: new_category.id, tags: [])
|
|
expect(post.reload.topic.category_id).to eq(new_category.id)
|
|
expect(post.topic.tags).to be_empty
|
|
end
|
|
|
|
it "returns an error if the topic does not have minimum amount of tags that the new category requires" do
|
|
SiteSetting.create_tag_allowed_groups = Group::AUTO_GROUPS[:trust_level_0]
|
|
SiteSetting.tag_topic_allowed_groups = Group::AUTO_GROUPS[:trust_level_0]
|
|
|
|
old_category = Fabricate(:category, minimum_required_tags: 0)
|
|
new_category = Fabricate(:category, minimum_required_tags: 1)
|
|
|
|
post = create_post(category: old_category)
|
|
topic = post.topic
|
|
|
|
post.revise(post.user, category_id: new_category.id)
|
|
expect(topic.errors.full_messages).to eq([I18n.t("tags.minimum_required_tags", count: 1)])
|
|
end
|
|
|
|
it "returns an error if the topic has tags not allowed in the new category" do
|
|
SiteSetting.create_tag_allowed_groups = Group::AUTO_GROUPS[:trust_level_0]
|
|
SiteSetting.tag_topic_allowed_groups = Group::AUTO_GROUPS[:trust_level_0]
|
|
|
|
tag1 = Fabricate(:tag)
|
|
tag2 = Fabricate(:tag)
|
|
tag_group = Fabricate(:tag_group, tags: [tag1])
|
|
tag_group2 = Fabricate(:tag_group, tags: [tag2])
|
|
|
|
old_category = Fabricate(:category, tag_groups: [tag_group])
|
|
new_category = Fabricate(:category, tag_groups: [tag_group2])
|
|
|
|
post = create_post(category: old_category, tags: [tag1.name])
|
|
topic = post.topic
|
|
|
|
post.revise(post.user, category_id: new_category.id)
|
|
expect(topic.errors.full_messages).to eq(
|
|
[
|
|
I18n.t(
|
|
"tags.forbidden.restricted_tags_cannot_be_used_in_category",
|
|
count: 1,
|
|
tags: tag1.name,
|
|
category: new_category.name,
|
|
),
|
|
],
|
|
)
|
|
end
|
|
|
|
it "returns an error if the topic is missing tags required from a tag group in the new category" do
|
|
SiteSetting.create_tag_allowed_groups = Group::AUTO_GROUPS[:trust_level_0]
|
|
SiteSetting.tag_topic_allowed_groups = Group::AUTO_GROUPS[:trust_level_0]
|
|
|
|
tag1 = Fabricate(:tag)
|
|
tag_group = Fabricate(:tag_group, tags: [tag1])
|
|
|
|
old_category = Fabricate(:category)
|
|
new_category =
|
|
Fabricate(
|
|
:category,
|
|
category_required_tag_groups: [
|
|
CategoryRequiredTagGroup.new(tag_group: tag_group, min_count: 1),
|
|
],
|
|
)
|
|
|
|
post = create_post(category: old_category)
|
|
topic = post.topic
|
|
|
|
post.revise(post.user, category_id: new_category.id)
|
|
expect(topic.errors.full_messages).to eq(
|
|
[
|
|
I18n.t(
|
|
"tags.required_tags_from_group",
|
|
count: 1,
|
|
tag_group_name: tag_group.name,
|
|
tags: tag1.name,
|
|
),
|
|
],
|
|
)
|
|
end
|
|
end
|
|
|
|
describe "editing tags" do
|
|
subject(:post_revisor) { PostRevisor.new(post) }
|
|
|
|
fab!(:post)
|
|
|
|
before do
|
|
Jobs.run_immediately!
|
|
|
|
TopicUser.change(
|
|
newuser.id,
|
|
post.topic_id,
|
|
notification_level: TopicUser.notification_levels[:watching],
|
|
)
|
|
end
|
|
|
|
it "creates notifications" do
|
|
expect { post_revisor.revise!(admin, tags: ["new-tag"]) }.to change { Notification.count }.by(
|
|
1,
|
|
)
|
|
end
|
|
|
|
it "skips notifications if disable_tags_edit_notifications" do
|
|
SiteSetting.disable_tags_edit_notifications = true
|
|
|
|
expect { post_revisor.revise!(admin, tags: ["new-tag"]) }.not_to change { Notification.count }
|
|
end
|
|
|
|
it "doesn't create a small_action post when create_post_for_category_and_tag_changes is false" do
|
|
SiteSetting.create_post_for_category_and_tag_changes = false
|
|
|
|
expect { post_revisor.revise!(admin, tags: ["new-tag"]) }.not_to change { Post.count }
|
|
end
|
|
|
|
it "edits a topic's tags" do
|
|
tag = Fabricate(:tag)
|
|
post_revisor.revise!(admin, tags: [{ id: tag.id, name: "outdated" }])
|
|
expect(post.topic.reload.tags).to contain_exactly(tag)
|
|
|
|
post_revisor.revise!(admin, tags: ["a-whole-new-tag"])
|
|
expect(post.topic.reload.tags).to match_array([have_attributes(name: "a-whole-new-tag")])
|
|
end
|
|
|
|
describe "when `create_post_for_category_and_tag_changes` site setting is enabled" do
|
|
fab!(:tag1) { Fabricate(:tag, name: "First tag") }
|
|
fab!(:tag2) { Fabricate(:tag, name: "Second tag") }
|
|
|
|
before do
|
|
SiteSetting.create_post_for_category_and_tag_changes = true
|
|
SiteSetting.whispers_allowed_groups = Group::AUTO_GROUPS[:staff]
|
|
end
|
|
|
|
it "Creates a small_action post with correct translation when both adding and removing tags" do
|
|
post.topic.update!(tags: [tag1])
|
|
|
|
expect { post_revisor.revise!(admin, tags: [tag2.name]) }.to change {
|
|
Post.where(topic_id: post.topic_id, action_code: "tags_changed").count
|
|
}.by(1)
|
|
|
|
expect(post.topic.ordered_posts.last.raw).to eq(
|
|
I18n.t(
|
|
"topic_tag_changed.added_and_removed",
|
|
added: "##{tag2.name}",
|
|
removed: "##{tag1.name}",
|
|
),
|
|
)
|
|
end
|
|
|
|
it "Creates a small_action post with correct translation when adding tags" do
|
|
post.topic.update!(tags: [])
|
|
|
|
expect { post_revisor.revise!(admin, tags: [tag1.name]) }.to change {
|
|
Post.where(topic_id: post.topic_id, action_code: "tags_changed").count
|
|
}.by(1)
|
|
|
|
expect(post.topic.ordered_posts.last.raw).to eq(
|
|
I18n.t("topic_tag_changed.added", added: "##{tag1.name}"),
|
|
)
|
|
end
|
|
|
|
it "Creates a small_action post with correct translation when removing tags" do
|
|
post.topic.update!(tags: [tag1, tag2])
|
|
|
|
expect { post_revisor.revise!(admin, tags: []) }.to change {
|
|
Post.where(topic_id: post.topic_id, action_code: "tags_changed").count
|
|
}.by(1)
|
|
|
|
expect(post.topic.ordered_posts.last.raw).to eq(
|
|
I18n.t("topic_tag_changed.removed", removed: "##{tag1.name}, ##{tag2.name}"),
|
|
)
|
|
end
|
|
|
|
it "Creates a small_action post when category is changed" do
|
|
current_category = post.topic.category
|
|
category = Fabricate(:category)
|
|
|
|
expect { post_revisor.revise!(admin, category_id: category.id) }.to change {
|
|
Post.where(topic_id: post.topic_id, action_code: "category_changed").count
|
|
}.by(1)
|
|
|
|
expect(post.topic.ordered_posts.last.raw).to eq(
|
|
I18n.t(
|
|
"topic_category_changed",
|
|
to: "##{category.slug}",
|
|
from: "##{current_category.slug}",
|
|
),
|
|
)
|
|
end
|
|
|
|
it "Creates a small_action as a whisper when category is changed" do
|
|
category = Fabricate(:category)
|
|
|
|
expect { post_revisor.revise!(admin, category_id: category.id) }.to change {
|
|
Post.where(topic_id: post.topic_id, action_code: "category_changed").count
|
|
}.by(1)
|
|
|
|
expect(post.topic.ordered_posts.last.post_type).to eq(Post.types[:whisper])
|
|
end
|
|
|
|
it "does not create a small_action or notification when a restricted tag is rejected" do
|
|
allowed_tag = Fabricate(:tag, name: "allowed-tag")
|
|
|
|
post.topic.update!(tags: [allowed_tag])
|
|
|
|
category = post.topic.category
|
|
category.update!(allow_global_tags: false)
|
|
category.tags = [allowed_tag]
|
|
category.save!
|
|
|
|
expect do
|
|
post_revisor.revise!(admin, tags: [allowed_tag.name, "disallowed-tag"])
|
|
end.not_to change { Post.where(topic_id: post.topic_id, action_code: "tags_changed").count }
|
|
|
|
expect(post.topic.reload.tags.pluck(:name)).to contain_exactly(allowed_tag.name)
|
|
expect(Jobs::NotifyTagChange.jobs.size).to eq(0)
|
|
end
|
|
|
|
describe "with PMs" do
|
|
fab!(:pm, :private_message_topic)
|
|
let(:first_post) { create_post(user: admin, topic: pm, allow_uncategorized_topics: false) }
|
|
fab!(:category) { Fabricate(:category, topic_count: 1) }
|
|
it "Does not create a category change small_action post when converting to a topic" do
|
|
expect do
|
|
TopicConverter.new(first_post.topic, admin).convert_to_public_topic(category.id)
|
|
end.to change { category.reload.topic_count }.by(1)
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
describe "editing locale" do
|
|
it "updates the post's locale" do
|
|
post = Fabricate(:post)
|
|
|
|
PostRevisor.new(post).revise!(post.user, locale: "ja")
|
|
|
|
post.reload
|
|
expect(post.locale).to eq("ja")
|
|
end
|
|
|
|
it "also updates the topic's locale if first post" do
|
|
post = Fabricate(:post)
|
|
|
|
PostRevisor.new(post).revise!(post.user, locale: "ja")
|
|
|
|
post.reload
|
|
expect(post.topic.locale).to eq("ja")
|
|
end
|
|
end
|
|
|
|
describe "revise wiki" do
|
|
before { SiteSetting.unique_posts_mins = 10 }
|
|
|
|
it "allows the user to change it to a wiki" do
|
|
pc =
|
|
PostCreator.new(newuser, topic_id: topic.id, raw: "this is a post that will become a wiki")
|
|
post = pc.create
|
|
expect(post.revise(post.user, wiki: true)).to be_truthy
|
|
post.reload
|
|
expect(post.wiki).to be_truthy
|
|
end
|
|
end
|
|
|
|
describe "revise" do
|
|
subject(:post_revisor) { PostRevisor.new(post) }
|
|
|
|
let(:post) { Fabricate(:post, post_args) }
|
|
let(:first_version_at) { post.last_version_at }
|
|
|
|
it "destroys last revision if edit is undone" do
|
|
old_raw = post.raw
|
|
|
|
post_revisor.revise!(admin, raw: "new post body", tags: ["new-tag"])
|
|
expect(post.topic.reload.tags.map(&:name)).to contain_exactly("new-tag")
|
|
expect(post.post_revisions.reload.size).to eq(1)
|
|
expect(post_revisor.raw_changed?).to eq(true)
|
|
|
|
post_revisor.revise!(admin, raw: old_raw, tags: [])
|
|
expect(post.topic.reload.tags.map(&:name)).to be_empty
|
|
expect(post.post_revisions.reload.size).to eq(0)
|
|
|
|
post_revisor.revise!(admin, raw: "next post body", tags: ["new-tag"])
|
|
expect(post.topic.reload.tags.map(&:name)).to contain_exactly("new-tag")
|
|
expect(post.post_revisions.reload.size).to eq(1)
|
|
end
|
|
|
|
describe "with the same body" do
|
|
it "doesn't change version" do
|
|
expect {
|
|
expect(post_revisor.revise!(post.user, raw: post.raw)).to eq(false)
|
|
post.reload
|
|
}.not_to change(post, :version)
|
|
end
|
|
end
|
|
|
|
describe "with nil raw contents" do
|
|
it "doesn't change version" do
|
|
expect {
|
|
expect(post_revisor.revise!(post.user, raw: nil)).to eq(false)
|
|
post.reload
|
|
}.not_to change(post, :version)
|
|
end
|
|
end
|
|
|
|
describe "topic is in slow mode" do
|
|
before { topic.update!(slow_mode_seconds: 1000) }
|
|
|
|
it "regular edits are not allowed by default" do
|
|
post_revisor.revise!(
|
|
post.user,
|
|
{ raw: "updated body" },
|
|
revised_at: post.updated_at + 1000.minutes,
|
|
)
|
|
|
|
post.reload
|
|
expect(post.errors.present?).to eq(true)
|
|
expect(post.errors.messages[:base].first).to be I18n.t("cannot_edit_on_slow_mode")
|
|
end
|
|
|
|
it "grace period editing is allowed" do
|
|
SiteSetting.editing_grace_period = 1.minute
|
|
|
|
post_revisor.revise!(
|
|
post.user,
|
|
{ raw: "updated body" },
|
|
revised_at: post.updated_at + 10.seconds,
|
|
)
|
|
|
|
post.reload
|
|
expect(post.errors).to be_empty
|
|
end
|
|
|
|
it "regular edits are allowed if it was turned on in settings" do
|
|
SiteSetting.slow_mode_prevents_editing = false
|
|
|
|
post_revisor.revise!(
|
|
post.user,
|
|
{ raw: "updated body" },
|
|
revised_at: post.updated_at + 10.minutes,
|
|
)
|
|
|
|
post.reload
|
|
expect(post.errors).to be_empty
|
|
end
|
|
|
|
it "staff is allowed to edit posts even if the topic is in slow mode" do
|
|
admin = Fabricate(:admin)
|
|
post_revisor.revise!(
|
|
admin,
|
|
{ raw: "updated body" },
|
|
revised_at: post.updated_at + 10.minutes,
|
|
)
|
|
|
|
post.reload
|
|
expect(post.errors).to be_empty
|
|
end
|
|
end
|
|
|
|
describe "grace period editing" do
|
|
it "correctly applies edits" do
|
|
SiteSetting.editing_grace_period = 1.minute
|
|
|
|
post_revisor.revise!(
|
|
post.user,
|
|
{ raw: "updated body" },
|
|
revised_at: post.updated_at + 10.seconds,
|
|
)
|
|
post.reload
|
|
|
|
expect(post.version).to eq(1)
|
|
expect(post.public_version).to eq(1)
|
|
expect(post.revisions.size).to eq(0)
|
|
expect(post.last_version_at).to eq_time(first_version_at)
|
|
expect(post_revisor.category_changed).to be_blank
|
|
end
|
|
|
|
it "does create a new version if a large diff happens" do
|
|
SiteSetting.editing_grace_period_max_diff = 10
|
|
|
|
post = Fabricate(:post, raw: "hello world")
|
|
revisor = PostRevisor.new(post)
|
|
revisor.revise!(
|
|
post.user,
|
|
{ raw: "hello world123456789" },
|
|
revised_at: post.updated_at + 1.second,
|
|
)
|
|
|
|
post.reload
|
|
|
|
expect(post.version).to eq(1)
|
|
|
|
revisor = PostRevisor.new(post)
|
|
revisor.revise!(
|
|
post.user,
|
|
{ raw: "hello world12345678901" },
|
|
revised_at: post.updated_at + 1.second,
|
|
)
|
|
|
|
post.reload
|
|
expect(post.version).to eq(2)
|
|
|
|
expect(post.revisions.first.modifications["raw"][0]).to eq("hello world")
|
|
expect(post.revisions.first.modifications["cooked"][0]).to eq("<p>hello world</p>")
|
|
|
|
SiteSetting.editing_grace_period_max_diff_high_trust = 100
|
|
|
|
post.user.update_columns(trust_level: 2)
|
|
|
|
revisor = PostRevisor.new(post)
|
|
revisor.revise!(
|
|
post.user,
|
|
{ raw: "hello world12345678901 123456789012" },
|
|
revised_at: post.updated_at + 1.second,
|
|
)
|
|
|
|
post.reload
|
|
expect(post.version).to eq(2)
|
|
expect(post.revisions.count).to eq(1)
|
|
end
|
|
|
|
it "creates a new version when diff computation exceeds the comparison budget" do
|
|
SiteSetting.editing_grace_period = 1.minute
|
|
SiteSetting.editing_grace_period_max_diff = 1_000
|
|
|
|
post = Fabricate(:post, raw: "hello world")
|
|
revisor = PostRevisor.new(post)
|
|
|
|
ONPDiff
|
|
.any_instance
|
|
.stubs(:short_diff)
|
|
.raises(
|
|
ONPDiff::DiffLimitExceeded.new(
|
|
comparisons_used: 2_000_001,
|
|
comparison_budget: 2_000_000,
|
|
left_size: 11,
|
|
right_size: 12,
|
|
),
|
|
)
|
|
|
|
revisor.revise!(post.user, { raw: "hello world!" }, revised_at: post.updated_at + 1.second)
|
|
|
|
post.reload
|
|
expect(post.version).to eq(2)
|
|
expect(post.revisions.count).to eq(1)
|
|
end
|
|
|
|
it "creates a new version when the post is flagged" do
|
|
SiteSetting.editing_grace_period = 1.minute
|
|
|
|
post = Fabricate(:post, raw: "hello world")
|
|
|
|
Fabricate(:flag_post_action, post: post, user: user)
|
|
|
|
revisor = PostRevisor.new(post)
|
|
revisor.revise!(
|
|
post.user,
|
|
{ raw: "hello world, JK" },
|
|
revised_at: post.updated_at + 1.second,
|
|
)
|
|
|
|
post.reload
|
|
expect(post.version).to eq(2)
|
|
expect(post.revisions.count).to eq(1)
|
|
end
|
|
|
|
it "doesn't create a new version" do
|
|
SiteSetting.editing_grace_period = 1.minute
|
|
SiteSetting.editing_grace_period_max_diff = 100
|
|
|
|
# making a revision
|
|
post_revisor.revise!(
|
|
post.user,
|
|
{ raw: "updated body" },
|
|
revised_at: post.updated_at + SiteSetting.editing_grace_period + 1.second,
|
|
)
|
|
# "roll back"
|
|
post_revisor.revise!(
|
|
post.user,
|
|
{ raw: "Hello world" },
|
|
revised_at: post.updated_at + SiteSetting.editing_grace_period + 2.seconds,
|
|
)
|
|
|
|
post.reload
|
|
|
|
expect(post.version).to eq(1)
|
|
expect(post.public_version).to eq(1)
|
|
expect(post.revisions.size).to eq(0)
|
|
end
|
|
|
|
it "does not create a new version for tag-only topic change within grace period when tags unchanged" do
|
|
SiteSetting.tagging_enabled = true
|
|
SiteSetting.editing_grace_period = 1.minute
|
|
|
|
post_revisor.revise!(post.user, { tags: [] }, revised_at: post.updated_at + 10.seconds)
|
|
post.reload
|
|
|
|
expect(post.version).to eq(1)
|
|
expect(post.revisions.size).to eq(0)
|
|
end
|
|
end
|
|
|
|
describe "edit reasons" do
|
|
it "does create a new version if an edit reason is provided" do
|
|
post = Fabricate(:post, raw: "hello world")
|
|
revisor = PostRevisor.new(post)
|
|
revisor.revise!(
|
|
post.user,
|
|
{ raw: "hello world123456789", edit_reason: "this is my reason" },
|
|
revised_at: post.updated_at + 1.second,
|
|
)
|
|
post.reload
|
|
expect(post.version).to eq(2)
|
|
expect(post.revisions.count).to eq(1)
|
|
end
|
|
|
|
it "resets the edit_reason attribute in post model" do
|
|
freeze_time
|
|
SiteSetting.editing_grace_period = 5.seconds
|
|
post = Fabricate(:post, raw: "hello world")
|
|
revisor = PostRevisor.new(post)
|
|
revisor.revise!(
|
|
post.user,
|
|
{ raw: "hello world123456789", edit_reason: "this is my reason" },
|
|
revised_at: post.updated_at + 1.second,
|
|
)
|
|
post.reload
|
|
expect(post.edit_reason).to eq("this is my reason")
|
|
|
|
revisor.revise!(
|
|
post.user,
|
|
{ raw: "hello world4321" },
|
|
revised_at: post.updated_at + 7.seconds,
|
|
)
|
|
post.reload
|
|
expect(post.edit_reason).not_to be_present
|
|
end
|
|
|
|
it "does not create a new version if an edit reason is provided and its the same as the current edit reason" do
|
|
post = Fabricate(:post, raw: "hello world", edit_reason: "this is my reason")
|
|
revisor = PostRevisor.new(post)
|
|
revisor.revise!(
|
|
post.user,
|
|
{ raw: "hello world123456789", edit_reason: "this is my reason" },
|
|
revised_at: post.updated_at + 1.second,
|
|
)
|
|
post.reload
|
|
expect(post.version).to eq(1)
|
|
expect(post.revisions.count).to eq(0)
|
|
end
|
|
|
|
it "does not clobber the existing edit reason for a revision if it is not provided in a subsequent revision" do
|
|
post = Fabricate(:post, raw: "hello world")
|
|
revisor = PostRevisor.new(post)
|
|
revisor.revise!(
|
|
post.user,
|
|
{ raw: "hello world123456789", edit_reason: "this is my reason" },
|
|
revised_at: post.updated_at + 1.second,
|
|
)
|
|
post.reload
|
|
revisor.revise!(
|
|
post.user,
|
|
{ raw: "hello some other thing" },
|
|
revised_at: post.updated_at + 1.second,
|
|
)
|
|
expect(post.revisions.first.modifications[:edit_reason]).to eq([nil, "this is my reason"])
|
|
end
|
|
end
|
|
|
|
describe "hidden post" do
|
|
it "correctly stores the modification value" do
|
|
post.update(hidden: true, hidden_reason_id: Post.hidden_reasons[:flag_threshold_reached])
|
|
revisor = PostRevisor.new(post)
|
|
revisor.revise!(post.user, { raw: "hello world" }, revised_at: post.updated_at + 11.minutes)
|
|
expect(post.revisions.first.modifications.symbolize_keys).to eq(
|
|
cooked: ["<p>Hello world</p>", "<p>hello world</p>"],
|
|
raw: ["Hello world", "hello world"],
|
|
)
|
|
end
|
|
end
|
|
|
|
describe "revision much later" do
|
|
let!(:revised_at) { post.updated_at + 2.minutes }
|
|
|
|
before do
|
|
SiteSetting.editing_grace_period = 1.minute
|
|
post_revisor.revise!(post.user, { raw: "updated body" }, revised_at: revised_at)
|
|
post.reload
|
|
end
|
|
|
|
it "doesn't update a category" do
|
|
expect(post_revisor.category_changed).to be_blank
|
|
end
|
|
|
|
it "updates the versions" do
|
|
expect(post.version).to eq(2)
|
|
expect(post.public_version).to eq(2)
|
|
end
|
|
|
|
it "creates a new revision" do
|
|
expect(post.revisions.size).to eq(1)
|
|
end
|
|
|
|
it "updates the last_version_at" do
|
|
expect(post.last_version_at.to_i).to eq(revised_at.to_i)
|
|
end
|
|
|
|
describe "new edit window" do
|
|
before do
|
|
post_revisor.revise!(
|
|
post.user,
|
|
{ raw: "yet another updated body" },
|
|
revised_at: revised_at,
|
|
)
|
|
post.reload
|
|
end
|
|
|
|
it "doesn't create a new version if you do another" do
|
|
expect(post.version).to eq(2)
|
|
expect(post.public_version).to eq(2)
|
|
end
|
|
|
|
it "doesn't change last_version_at" do
|
|
expect(post.last_version_at.to_i).to eq(revised_at.to_i)
|
|
end
|
|
|
|
it "doesn't update a category" do
|
|
expect(post_revisor.category_changed).to be_blank
|
|
end
|
|
|
|
context "after second window" do
|
|
let!(:new_revised_at) { revised_at + 2.minutes }
|
|
|
|
before do
|
|
post_revisor.revise!(
|
|
post.user,
|
|
{ raw: "yet another, another updated body" },
|
|
revised_at: new_revised_at,
|
|
)
|
|
post.reload
|
|
end
|
|
|
|
it "does create a new version after the edit window" do
|
|
expect(post.version).to eq(3)
|
|
expect(post.public_version).to eq(3)
|
|
end
|
|
|
|
it "does create a new version after the edit window" do
|
|
expect(post.last_version_at.to_i).to eq(new_revised_at.to_i)
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
describe "category topic" do
|
|
let!(:category) do
|
|
category = Fabricate(:category)
|
|
category.update_column(:topic_id, topic.id)
|
|
category
|
|
end
|
|
|
|
let(:new_description) { "this is my new description." }
|
|
|
|
it "should have no description by default" do
|
|
expect(category.description).to be_blank
|
|
end
|
|
|
|
context "with one paragraph description" do
|
|
before do
|
|
post_revisor.revise!(post.user, raw: new_description)
|
|
category.reload
|
|
end
|
|
|
|
it "returns the changed category info" do
|
|
expect(post_revisor.category_changed).to eq(category)
|
|
end
|
|
|
|
it "updates the description of the category" do
|
|
expect(category.description).to eq(new_description)
|
|
end
|
|
end
|
|
|
|
context "with multiple paragraph description" do
|
|
before do
|
|
post_revisor.revise!(post.user, raw: "#{new_description}\n\nOther content goes here.")
|
|
category.reload
|
|
end
|
|
|
|
it "returns the changed category info" do
|
|
expect(post_revisor.category_changed).to eq(category)
|
|
end
|
|
|
|
it "updates the description of the category" do
|
|
expect(category.description).to eq(new_description)
|
|
end
|
|
end
|
|
|
|
context "with invalid description without paragraphs" do
|
|
before do
|
|
post_revisor.revise!(post.user, raw: "# This is a title")
|
|
category.reload
|
|
end
|
|
|
|
it "returns a error for the user" do
|
|
expect(post.errors.present?).to eq(true)
|
|
expect(post.errors.messages[:base].first).to be I18n.t(
|
|
"category.errors.description_incomplete",
|
|
)
|
|
end
|
|
|
|
it "doesn't update the description of the category" do
|
|
expect(category.description).to eq(nil)
|
|
end
|
|
end
|
|
|
|
context "when updating back to the original paragraph" do
|
|
before do
|
|
category.update_column(:description, "this is my description")
|
|
post_revisor.revise!(post.user, raw: Category.post_template)
|
|
category.reload
|
|
end
|
|
|
|
it "puts the description back to nothing" do
|
|
expect(category.description).to be_blank
|
|
end
|
|
|
|
it "returns the changed category info" do
|
|
expect(post_revisor.category_changed).to eq(category)
|
|
end
|
|
end
|
|
end
|
|
|
|
describe "rate limiter" do
|
|
fab!(:changed_by) { coding_horror }
|
|
|
|
before do
|
|
RateLimiter.enable
|
|
SiteSetting.editing_grace_period = 0
|
|
end
|
|
|
|
it "triggers a rate limiter" do
|
|
EditRateLimiter.any_instance.expects(:performed!)
|
|
post_revisor.revise!(changed_by, raw: "updated body")
|
|
end
|
|
|
|
it "raises error when a user gets rate limited" do
|
|
SiteSetting.max_edits_per_day = 1
|
|
user = Fabricate(:user, trust_level: 1)
|
|
|
|
post_revisor.revise!(user, raw: "body (edited)")
|
|
|
|
expect do post_revisor.revise!(user, raw: "body (edited twice) ") end.to raise_error(
|
|
RateLimiter::LimitExceeded,
|
|
)
|
|
end
|
|
|
|
it "edit limits scale up depending on user's trust level" do
|
|
SiteSetting.max_edits_per_day = 1
|
|
SiteSetting.tl2_additional_edits_per_day_multiplier = 2
|
|
SiteSetting.tl3_additional_edits_per_day_multiplier = 3
|
|
SiteSetting.tl4_additional_edits_per_day_multiplier = 4
|
|
|
|
user = Fabricate(:user, trust_level: 2)
|
|
expect { post_revisor.revise!(user, raw: "body (edited)") }.to_not raise_error
|
|
expect { post_revisor.revise!(user, raw: "body (edited twice)") }.to_not raise_error
|
|
expect do post_revisor.revise!(user, raw: "body (edited three times) ") end.to raise_error(
|
|
RateLimiter::LimitExceeded,
|
|
)
|
|
|
|
user = Fabricate(:user, trust_level: 3)
|
|
expect { post_revisor.revise!(user, raw: "body (edited)") }.to_not raise_error
|
|
expect { post_revisor.revise!(user, raw: "body (edited twice)") }.to_not raise_error
|
|
expect { post_revisor.revise!(user, raw: "body (edited three times)") }.to_not raise_error
|
|
expect do post_revisor.revise!(user, raw: "body (edited four times) ") end.to raise_error(
|
|
RateLimiter::LimitExceeded,
|
|
)
|
|
|
|
user = Fabricate(:user, trust_level: 4)
|
|
expect { post_revisor.revise!(user, raw: "body (edited)") }.to_not raise_error
|
|
expect { post_revisor.revise!(user, raw: "body (edited twice)") }.to_not raise_error
|
|
expect { post_revisor.revise!(user, raw: "body (edited three times)") }.to_not raise_error
|
|
expect { post_revisor.revise!(user, raw: "body (edited four times)") }.to_not raise_error
|
|
expect do post_revisor.revise!(user, raw: "body (edited five times) ") end.to raise_error(
|
|
RateLimiter::LimitExceeded,
|
|
)
|
|
end
|
|
end
|
|
|
|
describe "admin editing a new user's post" do
|
|
fab!(:changed_by, :admin)
|
|
|
|
before do
|
|
SiteSetting.newuser_max_embedded_media = 0
|
|
url = "http://i.imgur.com/wfn7rgU.jpg"
|
|
Oneboxer.stubs(:onebox).with(url, anything).returns("<img src='#{url}'>")
|
|
post_revisor.revise!(changed_by, raw: "So, post them here!\n#{url}")
|
|
end
|
|
|
|
it "allows an admin to insert images into a new user's post" do
|
|
expect(post.errors).to be_blank
|
|
end
|
|
|
|
it "marks the admin as the last updater" do
|
|
expect(post.last_editor_id).to eq(changed_by.id)
|
|
end
|
|
end
|
|
|
|
describe "new user editing their own post" do
|
|
before do
|
|
SiteSetting.newuser_max_embedded_media = 0
|
|
url = "http://i.imgur.com/FGg7Vzu.gif"
|
|
Oneboxer.stubs(:cached_onebox).with(url, anything).returns("<img src='#{url}'>")
|
|
post_revisor.revise!(post.user, raw: "So, post them here!\n#{url}")
|
|
end
|
|
|
|
it "doesn't allow images to be inserted" do
|
|
expect(post.errors).to be_present
|
|
end
|
|
end
|
|
|
|
describe "with a new body" do
|
|
before { SiteSetting.editing_grace_period_max_diff = 1000 }
|
|
|
|
fab!(:changed_by) { coding_horror }
|
|
let!(:result) { post_revisor.revise!(changed_by, raw: "lets update the body. Здравствуйте") }
|
|
|
|
it "correctly updates raw" do
|
|
expect(result).to eq(true)
|
|
expect(post.raw).to eq("lets update the body. Здравствуйте")
|
|
expect(post.invalidate_oneboxes).to eq(true)
|
|
expect(post.version).to eq(2)
|
|
expect(post.public_version).to eq(2)
|
|
expect(post.revisions.size).to eq(1)
|
|
expect(post.revisions.first.user_id).to eq(changed_by.id)
|
|
|
|
# updates word count
|
|
expect(post.word_count).to eq(5)
|
|
post.topic.reload
|
|
expect(post.topic.word_count).to eq(5)
|
|
end
|
|
|
|
it "increases the post_edits stat count" do
|
|
expect do post_revisor.revise!(post.user, { raw: "This is a new revision" }) end.to change {
|
|
post.user.user_stat.post_edits_count.to_i
|
|
}.by(1)
|
|
end
|
|
|
|
context "when second poster posts again quickly" do
|
|
it "is a grace period edit, because the second poster posted again quickly" do
|
|
SiteSetting.editing_grace_period = 1.minute
|
|
post_revisor.revise!(
|
|
changed_by,
|
|
{ raw: "yet another updated body" },
|
|
revised_at: post.updated_at + 10.seconds,
|
|
)
|
|
post.reload
|
|
expect(post.version).to eq(2)
|
|
expect(post.public_version).to eq(2)
|
|
expect(post.revisions.size).to eq(1)
|
|
end
|
|
end
|
|
|
|
context "when passing skip_revision as true" do
|
|
before do
|
|
SiteSetting.editing_grace_period = 1.minute
|
|
post_revisor.revise!(
|
|
changed_by,
|
|
{ raw: "yet another updated body" },
|
|
revised_at: post.updated_at + 10.hours,
|
|
skip_revision: true,
|
|
)
|
|
post.reload
|
|
end
|
|
|
|
it "does not create new revision " do
|
|
expect(post.version).to eq(2)
|
|
expect(post.public_version).to eq(2)
|
|
expect(post.revisions.size).to eq(1)
|
|
end
|
|
end
|
|
|
|
context "when editing the before_edit_post event signature" do
|
|
it "contains post and params" do
|
|
params = { raw: "body (edited)" }
|
|
events = DiscourseEvent.track_events { post_revisor.revise!(user, params) }
|
|
expect(events).to include(event_name: :before_edit_post, params: [post, params])
|
|
end
|
|
end
|
|
|
|
context "when editing the post_edited event signature for extensibility" do
|
|
it "exposes revise opts via the PostRevisor payload" do
|
|
params = { raw: "body (edited)" }
|
|
opts = { suggested_edit: true }
|
|
|
|
events = DiscourseEvent.track_events { post_revisor.revise!(user, params, opts) }
|
|
event = events.find { |e| e[:event_name] == :post_edited }
|
|
|
|
expect(event[:params].third).to be_kind_of(PostRevisor)
|
|
expect(event[:params].third.opts).to include(suggested_edit: true)
|
|
end
|
|
end
|
|
end
|
|
|
|
describe "topic excerpt" do
|
|
it "topic excerpt is updated only if first post is revised" do
|
|
revisor = PostRevisor.new(post)
|
|
first_post = topic.first_post
|
|
expect {
|
|
revisor.revise!(
|
|
first_post.user,
|
|
{ raw: "Edit the first post" },
|
|
revised_at: first_post.updated_at + 10.seconds,
|
|
)
|
|
topic.reload
|
|
}.to change { topic.excerpt }
|
|
second_post = Fabricate(:post, post_args.merge(post_number: 2, topic_id: topic.id))
|
|
expect {
|
|
PostRevisor.new(second_post).revise!(second_post.user, raw: "Edit the 2nd post")
|
|
topic.reload
|
|
}.to_not change { topic.excerpt }
|
|
end
|
|
end
|
|
|
|
describe "changing post ownership" do
|
|
it "does not call Topic.reset_highest when only user_id is changed" do
|
|
new_owner = Fabricate(:user)
|
|
Topic.expects(:reset_highest).never
|
|
|
|
post_revisor.revise!(admin, user_id: new_owner.id)
|
|
end
|
|
|
|
it "calls Topic.reset_highest when user_id and other fields are changed" do
|
|
new_owner = Fabricate(:user)
|
|
Topic.expects(:reset_highest).once
|
|
|
|
post_revisor.revise!(admin, user_id: new_owner.id, raw: "updated body")
|
|
end
|
|
|
|
it "does not increment post_edits_count when system user changes ownership" do
|
|
new_owner = Fabricate(:user)
|
|
system_user = Discourse.system_user
|
|
|
|
expect do post_revisor.revise!(system_user, user_id: new_owner.id) end.not_to change {
|
|
system_user.user_stat.post_edits_count.to_i
|
|
}
|
|
end
|
|
end
|
|
|
|
it "doesn't strip starting whitespaces" do
|
|
post_revisor.revise!(post.user, raw: " <-- whitespaces --> ")
|
|
post.reload
|
|
expect(post.raw).to eq(" <-- whitespaces -->")
|
|
end
|
|
|
|
it "revises and tracks changes of topic titles" do
|
|
new_title = "New topic title"
|
|
result =
|
|
post_revisor.revise!(
|
|
post.user,
|
|
{ title: new_title },
|
|
revised_at: post.updated_at + 10.minutes,
|
|
)
|
|
|
|
expect(result).to eq(true)
|
|
post.reload
|
|
expect(post.topic.title).to eq(new_title)
|
|
expect(post.revisions.first.modifications["title"][1]).to eq(new_title)
|
|
expect(post_revisor.topic_title_changed?).to eq(true)
|
|
expect(post_revisor.raw_changed?).to eq(false)
|
|
end
|
|
|
|
it "revises and tracks changes of topic archetypes for staff" do
|
|
new_archetype = Archetype.banner
|
|
result =
|
|
post_revisor.revise!(
|
|
admin,
|
|
{ archetype: new_archetype },
|
|
revised_at: post.updated_at + 10.minutes,
|
|
)
|
|
|
|
expect(result).to eq(true)
|
|
post.reload
|
|
expect(post.topic.archetype).to eq(new_archetype)
|
|
expect(post.revisions.first.modifications["archetype"][1]).to eq(new_archetype)
|
|
expect(post_revisor.raw_changed?).to eq(false)
|
|
end
|
|
|
|
it "does not allow regular users to change topic archetype to banner" do
|
|
result =
|
|
post_revisor.revise!(
|
|
post.user,
|
|
{ archetype: Archetype.banner },
|
|
revised_at: post.updated_at + 10.minutes,
|
|
)
|
|
|
|
expect(result).to eq(false)
|
|
post.reload
|
|
expect(post.topic.archetype).to eq(Archetype.default)
|
|
end
|
|
|
|
it "revises and tracks changes of topic tags" do
|
|
post_revisor.revise!(admin, tags: ["new-tag"])
|
|
expect(post.post_revisions.last.modifications).to eq("tags" => [[], ["new-tag"]])
|
|
expect(post_revisor.raw_changed?).to eq(false)
|
|
|
|
post_revisor.revise!(admin, tags: %w[new-tag new-tag-2])
|
|
before, after = post.post_revisions.last.modifications["tags"]
|
|
expect(before).to contain_exactly("new-tag")
|
|
expect(after).to contain_exactly("new-tag", "new-tag-2")
|
|
expect(post_revisor.raw_changed?).to eq(false)
|
|
|
|
post_revisor.revise!(admin, tags: ["new-tag-3"])
|
|
before, after = post.post_revisions.last.modifications["tags"]
|
|
expect(before).to contain_exactly("new-tag", "new-tag-2")
|
|
expect(after).to contain_exactly("new-tag-3")
|
|
expect(post_revisor.raw_changed?).to eq(false)
|
|
end
|
|
|
|
it "tracks tag changes by IDs (integers) with revision storing names" do
|
|
tag1 = Fabricate(:tag, name: "existing-tag")
|
|
tag2 = Fabricate(:tag, name: "another-tag")
|
|
|
|
post.topic.update!(tags: [tag1])
|
|
|
|
post_revisor.revise!(admin, tags: [{ id: tag2.id, name: tag2.name }])
|
|
|
|
modifications = post.post_revisions.last.modifications
|
|
expect(modifications["tags"]).to eq([["existing-tag"], ["another-tag"]])
|
|
end
|
|
|
|
it "tracks tag changes from empty to tagged using objects" do
|
|
tag = Fabricate(:tag, name: "new-via-id")
|
|
|
|
post_revisor.revise!(admin, tags: [{ id: tag.id, name: tag.name }])
|
|
|
|
modifications = post.post_revisions.last.modifications
|
|
expect(modifications["tags"]).to eq([[], ["new-via-id"]])
|
|
end
|
|
|
|
it "tracks tag changes from tagged to empty" do
|
|
tag = Fabricate(:tag, name: "will-remove")
|
|
post.topic.update!(tags: [tag])
|
|
|
|
post_revisor.revise!(admin, tags: [])
|
|
|
|
modifications = post.post_revisions.last.modifications
|
|
expect(modifications["tags"]).to eq([["will-remove"], []])
|
|
end
|
|
|
|
describe "#publish_changes" do
|
|
let!(:post) { Fabricate(:post, topic: topic) }
|
|
|
|
it "should publish topic changes to clients" do
|
|
revisor = PostRevisor.new(topic.ordered_posts.first, topic)
|
|
|
|
message =
|
|
MessageBus
|
|
.track_publish("/topic/#{topic.id}") do
|
|
revisor.revise!(newuser, title: "this is a test topic")
|
|
end
|
|
.first
|
|
|
|
payload = message.data
|
|
expect(payload[:reload_topic]).to eq(true)
|
|
end
|
|
end
|
|
|
|
context "when logging staff edits" do
|
|
it "doesn't log when a regular user revises a post" do
|
|
post_revisor.revise!(post.user, raw: "lets totally update the body")
|
|
log =
|
|
UserHistory.where(acting_user_id: post.user.id, action: UserHistory.actions[:post_edit])
|
|
expect(log).to be_blank
|
|
end
|
|
|
|
it "logs an edit when a staff member revises a post" do
|
|
post_revisor.revise!(moderator, raw: "lets totally update the body")
|
|
log =
|
|
UserHistory.where(
|
|
acting_user_id: moderator.id,
|
|
action: UserHistory.actions[:post_edit],
|
|
).first
|
|
expect(log).to be_present
|
|
expect(log.details).to eq("Hello world\n\n---\n\nlets totally update the body")
|
|
end
|
|
|
|
it "doesn't log an edit when skip_staff_log is true" do
|
|
post_revisor.revise!(
|
|
moderator,
|
|
{ raw: "lets totally update the body" },
|
|
skip_staff_log: true,
|
|
)
|
|
log =
|
|
UserHistory.where(
|
|
acting_user_id: moderator.id,
|
|
action: UserHistory.actions[:post_edit],
|
|
).first
|
|
expect(log).to be_blank
|
|
end
|
|
|
|
it "doesn't log an edit when a staff member edits their own post" do
|
|
revisor = PostRevisor.new(Fabricate(:post, user: moderator))
|
|
revisor.revise!(moderator, raw: "my own edit to my own thing")
|
|
|
|
log =
|
|
UserHistory.where(acting_user_id: moderator.id, action: UserHistory.actions[:post_edit])
|
|
expect(log).to be_blank
|
|
end
|
|
end
|
|
|
|
context "when logging group moderator edits" do
|
|
fab!(:group_user)
|
|
fab!(:category) { Fabricate(:category, topic: topic) }
|
|
fab!(:category_moderation_group) do
|
|
Fabricate(:category_moderation_group, category:, group: group_user.group)
|
|
end
|
|
|
|
before do
|
|
SiteSetting.enable_category_group_moderation = true
|
|
topic.update!(category: category)
|
|
post.update!(topic: topic)
|
|
end
|
|
|
|
it "logs an edit when a group moderator revises the category description" do
|
|
PostRevisor.new(post).revise!(
|
|
group_user.user,
|
|
raw: "a group moderator can update the description",
|
|
)
|
|
|
|
log =
|
|
UserHistory.where(
|
|
acting_user_id: group_user.user.id,
|
|
action: UserHistory.actions[:post_edit],
|
|
).first
|
|
expect(log).to be_present
|
|
expect(log.details).to eq(
|
|
"Hello world\n\n---\n\na group moderator can update the description",
|
|
)
|
|
end
|
|
end
|
|
|
|
context "with staff_edit_locks_post" do
|
|
context "when disabled" do
|
|
before { SiteSetting.staff_edit_locks_post = false }
|
|
|
|
it "does not lock the post when revised" do
|
|
result = post_revisor.revise!(moderator, raw: "lets totally update the body")
|
|
expect(result).to eq(true)
|
|
post.reload
|
|
expect(post).not_to be_locked
|
|
end
|
|
end
|
|
|
|
context "when enabled" do
|
|
before { SiteSetting.staff_edit_locks_post = true }
|
|
|
|
it "locks the post when revised by staff" do
|
|
result = post_revisor.revise!(moderator, raw: "lets totally update the body")
|
|
expect(result).to eq(true)
|
|
post.reload
|
|
expect(post).to be_locked
|
|
end
|
|
|
|
it "doesn't lock the wiki posts" do
|
|
post.wiki = true
|
|
result = post_revisor.revise!(moderator, raw: "some new raw content")
|
|
expect(result).to eq(true)
|
|
post.reload
|
|
expect(post).not_to be_locked
|
|
end
|
|
|
|
it "doesn't lock the post when the raw did not change" do
|
|
result = post_revisor.revise!(moderator, title: "New topic title, cool!")
|
|
expect(result).to eq(true)
|
|
post.reload
|
|
expect(post.topic.title).to eq("New topic title, cool!")
|
|
expect(post).not_to be_locked
|
|
end
|
|
|
|
it "doesn't lock the post when revised by a regular user" do
|
|
result = post_revisor.revise!(user, raw: "lets totally update the body")
|
|
expect(result).to eq(true)
|
|
post.reload
|
|
expect(post).not_to be_locked
|
|
end
|
|
|
|
it "doesn't lock the post when revised by system user" do
|
|
result =
|
|
post_revisor.revise!(Discourse.system_user, raw: "I usually replace hotlinked images")
|
|
expect(result).to eq(true)
|
|
post.reload
|
|
expect(post).not_to be_locked
|
|
end
|
|
|
|
it "doesn't lock a staff member's post" do
|
|
staff_post = Fabricate(:post, user: moderator)
|
|
revisor = PostRevisor.new(staff_post)
|
|
|
|
result = revisor.revise!(moderator, raw: "lets totally update the body")
|
|
expect(result).to eq(true)
|
|
staff_post.reload
|
|
expect(staff_post).not_to be_locked
|
|
end
|
|
end
|
|
end
|
|
|
|
context "with alerts" do
|
|
fab!(:mentioned_user, :user)
|
|
|
|
before { Jobs.run_immediately! }
|
|
|
|
it "generates a notification for a mention" do
|
|
expect {
|
|
post_revisor.revise!(
|
|
user,
|
|
raw: "Random user is mentioning @#{mentioned_user.username_lower}",
|
|
)
|
|
}.to change { Notification.where(notification_type: Notification.types[:mentioned]).count }
|
|
end
|
|
|
|
it "never generates a notification for a mention when the System user revise a post" do
|
|
expect {
|
|
post_revisor.revise!(
|
|
Discourse.system_user,
|
|
raw: "System user is mentioning @#{mentioned_user.username_lower}",
|
|
)
|
|
}.not_to change {
|
|
Notification.where(notification_type: Notification.types[:mentioned]).count
|
|
}
|
|
end
|
|
end
|
|
|
|
context "with tagging" do
|
|
context "with tagging disabled" do
|
|
before { SiteSetting.tagging_enabled = false }
|
|
|
|
it "doesn't add the tags" do
|
|
result =
|
|
post_revisor.revise!(
|
|
user,
|
|
raw: "lets totally update the body",
|
|
tags: %w[totally update],
|
|
)
|
|
expect(result).to eq(true)
|
|
post.reload
|
|
expect(post.topic.tags.size).to eq(0)
|
|
end
|
|
end
|
|
|
|
context "with tagging enabled" do
|
|
before { SiteSetting.tagging_enabled = true }
|
|
|
|
context "when can create tags" do
|
|
before do
|
|
SiteSetting.create_tag_allowed_groups = "1|3|#{Group::AUTO_GROUPS[:trust_level_0]}"
|
|
SiteSetting.tag_topic_allowed_groups = "1|3|#{Group::AUTO_GROUPS[:trust_level_0]}"
|
|
end
|
|
|
|
it "can create all tags if none exist" do
|
|
expect {
|
|
@result =
|
|
post_revisor.revise!(
|
|
user,
|
|
raw: "lets totally update the body",
|
|
tags: %w[totally update],
|
|
)
|
|
}.to change { Tag.count }.by(2)
|
|
expect(@result).to eq(true)
|
|
post.reload
|
|
expect(post.topic.tags.map(&:name).sort).to eq(%w[totally update])
|
|
end
|
|
|
|
it "creates missing tags if some exist" do
|
|
Fabricate(:tag, name: "totally")
|
|
expect {
|
|
@result =
|
|
post_revisor.revise!(
|
|
user,
|
|
raw: "lets totally update the body",
|
|
tags: %w[totally update],
|
|
)
|
|
}.to change { Tag.count }.by(1)
|
|
expect(@result).to eq(true)
|
|
post.reload
|
|
expect(post.topic.tags.map(&:name).sort).to eq(%w[totally update])
|
|
end
|
|
|
|
it "can remove all tags" do
|
|
topic.tags = [Fabricate(:tag, name: "super"), Fabricate(:tag, name: "stuff")]
|
|
result = post_revisor.revise!(user, raw: "lets totally update the body", tags: [])
|
|
expect(result).to eq(true)
|
|
post.reload
|
|
expect(post.topic.tags.size).to eq(0)
|
|
end
|
|
|
|
it "can't add staff-only tags" do
|
|
create_staff_only_tags(["important"])
|
|
result =
|
|
post_revisor.revise!(
|
|
user,
|
|
raw: "lets totally update the body",
|
|
tags: %w[important stuff],
|
|
)
|
|
expect(result).to eq(false)
|
|
expect(post.topic.errors.present?).to eq(true)
|
|
end
|
|
|
|
it "staff can add staff-only tags" do
|
|
create_staff_only_tags(["important"])
|
|
result =
|
|
post_revisor.revise!(
|
|
admin,
|
|
raw: "lets totally update the body",
|
|
tags: %w[important stuff],
|
|
)
|
|
expect(result).to eq(true)
|
|
post.reload
|
|
expect(post.topic.tags.map(&:name).sort).to eq(%w[important stuff])
|
|
end
|
|
|
|
it "triggers the :post_edited event with topic_changed?" do
|
|
topic.tags = [Fabricate(:tag, name: "super"), Fabricate(:tag, name: "stuff")]
|
|
|
|
events =
|
|
DiscourseEvent.track_events do
|
|
post_revisor.revise!(user, raw: "lets totally update the body", tags: [])
|
|
end
|
|
|
|
event = events.find { |e| e[:event_name] == :post_edited }
|
|
|
|
expect(event[:params].first).to eq(post)
|
|
expect(event[:params].second).to eq(true)
|
|
expect(event[:params].third).to be_kind_of(PostRevisor)
|
|
expect(event[:params].third.topic_diff).to eq({ "tags" => [%w[super stuff], []] })
|
|
end
|
|
|
|
context "with staff-only tags" do
|
|
before do
|
|
create_staff_only_tags(["important"])
|
|
topic = post.topic
|
|
topic.tags = [
|
|
Fabricate(:tag, name: "super"),
|
|
Tag.where(name: "important").first,
|
|
Fabricate(:tag, name: "stuff"),
|
|
]
|
|
end
|
|
|
|
it "staff-only tags can't be removed" do
|
|
result =
|
|
post_revisor.revise!(user, raw: "lets totally update the body", tags: ["stuff"])
|
|
expect(result).to eq(false)
|
|
expect(post.topic.errors.present?).to eq(true)
|
|
post.reload
|
|
expect(post.topic.tags.map(&:name).sort).to eq(%w[important stuff super])
|
|
end
|
|
|
|
it "can't remove all tags if some are staff-only" do
|
|
result = post_revisor.revise!(user, raw: "lets totally update the body", tags: [])
|
|
expect(result).to eq(false)
|
|
expect(post.topic.errors.present?).to eq(true)
|
|
post.reload
|
|
expect(post.topic.tags.map(&:name).sort).to eq(%w[important stuff super])
|
|
end
|
|
|
|
it "staff-only tags can be removed by staff" do
|
|
result =
|
|
post_revisor.revise!(admin, raw: "lets totally update the body", tags: ["stuff"])
|
|
expect(result).to eq(true)
|
|
post.reload
|
|
expect(post.topic.tags.map(&:name)).to eq(["stuff"])
|
|
end
|
|
|
|
it "staff can remove all tags" do
|
|
result = post_revisor.revise!(admin, raw: "lets totally update the body", tags: [])
|
|
expect(result).to eq(true)
|
|
post.reload
|
|
expect(post.topic.tags.size).to eq(0)
|
|
end
|
|
end
|
|
|
|
context "with hidden tags" do
|
|
let(:bumped_at) { 1.day.ago }
|
|
|
|
before do
|
|
topic.update!(bumped_at: bumped_at)
|
|
create_hidden_tags(%w[important secret])
|
|
topic = post.topic
|
|
topic.tags = [
|
|
Fabricate(:tag, name: "super"),
|
|
Tag.where(name: "important").first,
|
|
Fabricate(:tag, name: "stuff"),
|
|
]
|
|
end
|
|
|
|
it "creates a hidden revision" do
|
|
post_revisor.revise!(
|
|
Fabricate(:admin),
|
|
raw: post.raw,
|
|
tags: topic.tags.map(&:name) + ["secret"],
|
|
)
|
|
expect(post.reload.revisions.first.hidden).to eq(true)
|
|
end
|
|
|
|
it "doesn't increment public_version for hidden revisions" do
|
|
post_revisor.revise!(admin, raw: post.raw, tags: topic.tags.map(&:name) + ["secret"])
|
|
post.reload
|
|
expect(post.version).to eq(2)
|
|
expect(post.public_version).to eq(1)
|
|
end
|
|
|
|
it "doesn't decrement public_version when hidden revision is destroyed" do
|
|
original_tags = topic.tags.map(&:name)
|
|
post_revisor.revise!(admin, raw: post.raw, tags: original_tags + ["secret"])
|
|
post.reload
|
|
expect(post.version).to eq(2)
|
|
expect(post.public_version).to eq(1)
|
|
expect(post.revisions.count).to eq(1)
|
|
|
|
post_revisor.revise!(admin, raw: post.raw, tags: original_tags)
|
|
post.reload
|
|
expect(post.version).to eq(1)
|
|
expect(post.public_version).to eq(1)
|
|
expect(post.revisions.count).to eq(0)
|
|
end
|
|
|
|
it "increments public_version when hidden tag added with other visible changes" do
|
|
post_revisor.revise!(
|
|
admin,
|
|
raw: "#{post.raw} with additional content",
|
|
tags: topic.tags.map(&:name) + ["secret"],
|
|
)
|
|
post.reload
|
|
expect(post.version).to eq(2)
|
|
expect(post.public_version).to eq(2)
|
|
expect(post.revisions.first.hidden).to eq(false)
|
|
end
|
|
|
|
it "doesn't notify topic owner about hidden tags" do
|
|
PostActionNotifier.enable
|
|
Jobs.run_immediately!
|
|
expect {
|
|
post_revisor.revise!(
|
|
Fabricate(:admin),
|
|
raw: post.raw,
|
|
tags: topic.tags.map(&:name) + ["secret"],
|
|
)
|
|
}.not_to change {
|
|
Notification.where(notification_type: Notification.types[:edited]).count
|
|
}
|
|
end
|
|
end
|
|
|
|
context "with required tag group" do
|
|
fab!(:tag1, :tag)
|
|
fab!(:tag2, :tag)
|
|
fab!(:tag3, :tag)
|
|
fab!(:tag_group) { Fabricate(:tag_group, tags: [tag1, tag2]) }
|
|
fab!(:category) do
|
|
Fabricate(
|
|
:category,
|
|
name: "beta",
|
|
category_required_tag_groups: [
|
|
CategoryRequiredTagGroup.new(tag_group: tag_group, min_count: 1),
|
|
],
|
|
)
|
|
end
|
|
|
|
before { post.topic.update(category: category) }
|
|
|
|
it "doesn't allow removing all tags from the group" do
|
|
post.topic.tags = [tag1, tag2]
|
|
result = post_revisor.revise!(user, raw: "lets totally update the body", tags: [])
|
|
expect(result).to eq(false)
|
|
end
|
|
|
|
it "allows removing some tags" do
|
|
post.topic.tags = [tag1, tag2, tag3]
|
|
result =
|
|
post_revisor.revise!(user, raw: "lets totally update the body", tags: [tag1.name])
|
|
expect(result).to eq(true)
|
|
expect(post.reload.topic.tags.map(&:name)).to eq([tag1.name])
|
|
end
|
|
|
|
it "allows admins to remove the tags" do
|
|
post.topic.tags = [tag1, tag2, tag3]
|
|
result = post_revisor.revise!(admin, raw: "lets totally update the body", tags: [])
|
|
expect(result).to eq(true)
|
|
expect(post.reload.topic.tags.size).to eq(0)
|
|
end
|
|
end
|
|
end
|
|
|
|
context "when cannot create tags" do
|
|
before do
|
|
SiteSetting.create_tag_allowed_groups = Group::AUTO_GROUPS[:trust_level_4]
|
|
SiteSetting.tag_topic_allowed_groups = Group::AUTO_GROUPS[:trust_level_0]
|
|
end
|
|
|
|
it "only uses existing tags" do
|
|
Fabricate(:tag, name: "totally")
|
|
expect {
|
|
@result =
|
|
post_revisor.revise!(
|
|
user,
|
|
raw: "lets totally update the body",
|
|
tags: %w[totally update],
|
|
)
|
|
}.to_not change { Tag.count }
|
|
expect(@result).to eq(true)
|
|
post.reload
|
|
expect(post.topic.tags.map(&:name)).to eq(["totally"])
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
context "with uploads" do
|
|
let(:image1) { Fabricate(:upload) }
|
|
let(:image2) { Fabricate(:upload) }
|
|
let(:image3) { Fabricate(:upload) }
|
|
let(:image4) { Fabricate(:upload) }
|
|
let(:post_args) { { user: user, topic: topic, raw: <<~RAW } }
|
|
This is a post with multiple uploads
|
|

|
|

|
|
RAW
|
|
|
|
it "updates linked post uploads" do
|
|
post.link_post_uploads
|
|
expect(post.upload_references.pluck(:upload_id)).to contain_exactly(image1.id, image2.id)
|
|
|
|
post_revisor.revise!(user, raw: <<~RAW)
|
|
This is a post with multiple uploads
|
|

|
|

|
|

|
|
RAW
|
|
|
|
expect(post.reload.upload_references.pluck(:upload_id)).to contain_exactly(
|
|
image2.id,
|
|
image3.id,
|
|
image4.id,
|
|
)
|
|
end
|
|
|
|
context "with secure uploads uploads" do
|
|
let!(:image5) { Fabricate(:secure_upload) }
|
|
before do
|
|
Jobs.run_immediately!
|
|
setup_s3
|
|
SiteSetting.authorized_extensions = "png|jpg|gif|mp4"
|
|
SiteSetting.secure_uploads = true
|
|
stub_upload(image5)
|
|
end
|
|
|
|
it "updates the upload secure status, which is secure by default from the composer. set to false for a public topic" do
|
|
stub_image_size
|
|
post_revisor.revise!(user, raw: <<~RAW)
|
|
This is a post with a secure upload
|
|

|
|
RAW
|
|
|
|
expect(image5.reload.secure).to eq(false)
|
|
expect(image5.security_last_changed_reason).to eq(
|
|
"access control post dictates security | source: post processor",
|
|
)
|
|
end
|
|
|
|
it "does not update the upload secure status, which is secure by default from the composer for a private" do
|
|
post.topic.update(category: Fabricate(:private_category, group: Fabricate(:group)))
|
|
stub_image_size
|
|
post_revisor.revise!(user, raw: <<~RAW)
|
|
This is a post with a secure upload
|
|

|
|
RAW
|
|
|
|
expect(image5.reload.secure).to eq(true)
|
|
expect(image5.security_last_changed_reason).to eq(
|
|
"access control post dictates security | source: post processor",
|
|
)
|
|
end
|
|
end
|
|
end
|
|
|
|
context "with drafts" do
|
|
it "does not advance draft sequence if keep_existing_draft option is true" do
|
|
post = Fabricate(:post, user: user)
|
|
topic = post.topic
|
|
draft_key = "topic_#{topic.id}"
|
|
data = { reply: "test 12222" }.to_json
|
|
Draft.set(user, draft_key, 0, data)
|
|
Draft.set(user, draft_key, 0, data)
|
|
expect {
|
|
PostRevisor.new(post).revise!(
|
|
post.user,
|
|
{ title: "updated title for my topic" },
|
|
keep_existing_draft: true,
|
|
)
|
|
}.to not_change {
|
|
Draft.where(user: user, draft_key: draft_key).first.sequence
|
|
}.and not_change {
|
|
DraftSequence.where(user_id: user.id, draft_key: draft_key).first.sequence
|
|
}
|
|
|
|
expect {
|
|
PostRevisor.new(post).revise!(post.user, { title: "updated title for my topic" })
|
|
}.to change { Draft.where(user: user, draft_key: draft_key).count }.from(1).to(
|
|
0,
|
|
).and change {
|
|
DraftSequence.where(user_id: user.id, draft_key: draft_key).first.sequence
|
|
}.by(1)
|
|
end
|
|
end
|
|
|
|
context "when skipping validations" do
|
|
fab!(:post) { Fabricate(:post, raw: "aaa", skip_validation: true) }
|
|
|
|
it "can revise multiple times and remove unnecessary revisions" do
|
|
post_revisor.revise!(admin, { raw: "bbb" }, skip_validations: true)
|
|
expect(post.errors).to be_empty
|
|
|
|
# Revert to old version which was invalid to destroy previously created
|
|
# post revision and trigger another post save.
|
|
post_revisor.revise!(admin, { raw: "aaa" }, skip_validations: true)
|
|
expect(post.errors).to be_empty
|
|
end
|
|
end
|
|
end
|
|
|
|
context "when the review_every_post setting is enabled" do
|
|
let(:post) { Fabricate(:post, post_args) }
|
|
let(:revisor) { PostRevisor.new(post) }
|
|
|
|
before { SiteSetting.review_every_post = true }
|
|
|
|
it "queues the post when a regular user edits it" do
|
|
expect {
|
|
revisor.revise!(
|
|
post.user,
|
|
{ raw: "updated body" },
|
|
revised_at: post.updated_at + 10.minutes,
|
|
)
|
|
}.to change(ReviewablePost, :count).by(1)
|
|
end
|
|
|
|
it "does nothing when a staff member edits a post" do
|
|
admin = Fabricate(:admin)
|
|
|
|
expect { revisor.revise!(admin, { raw: "updated body" }) }.not_to change(
|
|
ReviewablePost,
|
|
:count,
|
|
)
|
|
end
|
|
|
|
it "skips grace period edits" do
|
|
SiteSetting.editing_grace_period = 1.minute
|
|
|
|
expect {
|
|
revisor.revise!(
|
|
post.user,
|
|
{ raw: "updated body" },
|
|
revised_at: post.updated_at + 10.seconds,
|
|
)
|
|
}.not_to change(ReviewablePost, :count)
|
|
end
|
|
end
|
|
|
|
describe "topic bumping" do
|
|
subject(:post_revisor) { PostRevisor.new(post) }
|
|
|
|
let(:post) { Fabricate(:post, post_args) }
|
|
|
|
it "doesn't bump the topic when editing the last post" do
|
|
expect {
|
|
post_revisor.revise!(
|
|
post.user,
|
|
{ raw: "updated body" },
|
|
revised_at: post.updated_at + SiteSetting.editing_grace_period + 1.second,
|
|
)
|
|
}.not_to change { post.topic.bumped_at }
|
|
end
|
|
|
|
it "doesn't bump the topic when editing a post that isn't the last post" do
|
|
create_post(topic_id: post.topic.id)
|
|
expect {
|
|
post_revisor.revise!(
|
|
post.user,
|
|
{ raw: "updated body" },
|
|
revised_at: post.updated_at + SiteSetting.editing_grace_period + 1.second,
|
|
)
|
|
}.not_to change { post.topic.bumped_at }
|
|
end
|
|
|
|
it "doesn't bump the topic when editing the topic title" do
|
|
expect {
|
|
post_revisor.revise!(
|
|
post.user,
|
|
{ title: "This is an updated topic title" },
|
|
revised_at: post.updated_at + SiteSetting.editing_grace_period + 1.second,
|
|
)
|
|
}.not_to change { post.topic.bumped_at }
|
|
end
|
|
|
|
it "doesn't bump the topic when editing the topic category" do
|
|
expect {
|
|
post_revisor.revise!(
|
|
post.user,
|
|
{ category_id: Fabricate(:category).id },
|
|
revised_at: post.updated_at + SiteSetting.editing_grace_period + 1.second,
|
|
)
|
|
}.not_to change { post.topic.bumped_at }
|
|
end
|
|
|
|
it "doesn't bump the topic when editing tags" do
|
|
expect { post_revisor.revise!(post.user, { tags: %w[totally update] }) }.not_to change {
|
|
post.topic.bumped_at
|
|
}
|
|
end
|
|
|
|
describe "should_bump_topic plugin modifier" do
|
|
let(:plugin_instance) { Plugin::Instance.new }
|
|
let(:modifier_return_value) { nil }
|
|
let(:modifier_block) do
|
|
Proc.new do |value, modifier_post, modifier_post_changes, modifier_topic_changes, editor|
|
|
modifier_return_value
|
|
end
|
|
end
|
|
|
|
before { plugin_instance.register_modifier(:should_bump_topic, &modifier_block) }
|
|
|
|
after do
|
|
DiscoursePluginRegistry.unregister_modifier(
|
|
plugin_instance,
|
|
:should_bump_topic,
|
|
&modifier_block
|
|
)
|
|
end
|
|
|
|
context "when the modifier returns false" do
|
|
let(:modifier_return_value) { false }
|
|
|
|
it "prevents bumping" do
|
|
expect {
|
|
post_revisor.revise!(
|
|
post.user,
|
|
{ raw: "updated body" },
|
|
revised_at: post.updated_at + SiteSetting.editing_grace_period + 1.second,
|
|
)
|
|
}.not_to change { post.topic.bumped_at }
|
|
end
|
|
end
|
|
|
|
context "when the modifier returns true" do
|
|
let(:modifier_return_value) { true }
|
|
|
|
it "bumps the topic" do
|
|
expect {
|
|
post_revisor.revise!(
|
|
post.user,
|
|
{ raw: "updated body" },
|
|
revised_at: post.updated_at + SiteSetting.editing_grace_period + 1.second,
|
|
)
|
|
}.to change { post.topic.bumped_at }
|
|
end
|
|
end
|
|
end
|
|
|
|
context "for a wiki topic" do
|
|
before { post.update!(wiki: true) }
|
|
|
|
it "bumps the topic when the OP is edited" do
|
|
expect {
|
|
post_revisor.revise!(
|
|
post.user,
|
|
{ raw: "updated body" },
|
|
revised_at: post.updated_at + SiteSetting.editing_grace_period + 1.second,
|
|
)
|
|
}.to change { post.topic.bumped_at }
|
|
end
|
|
|
|
it "doesn't bump the topic when another post is edited" do
|
|
other_post = Fabricate(:post, topic: topic)
|
|
post_revisor_other = PostRevisor.new(other_post)
|
|
|
|
expect {
|
|
post_revisor_other.revise!(
|
|
post.user,
|
|
{ raw: "updated body" },
|
|
revised_at: post.updated_at + SiteSetting.editing_grace_period + 1.second,
|
|
)
|
|
}.not_to change { post.topic.bumped_at }
|
|
end
|
|
end
|
|
|
|
context "with hidden tags" do
|
|
let(:bumped_at) { 1.day.ago }
|
|
|
|
before do
|
|
post.topic.update!(bumped_at: bumped_at)
|
|
create_hidden_tags(%w[important secret])
|
|
post.topic.tags = [
|
|
Fabricate(:tag, name: "super"),
|
|
Tag.where(name: "important").first,
|
|
Fabricate(:tag, name: "stuff"),
|
|
]
|
|
end
|
|
|
|
it "doesn't bump topic if only staff-only tags are added" do
|
|
expect {
|
|
result =
|
|
post_revisor.revise!(
|
|
Fabricate(:admin),
|
|
raw: post.raw,
|
|
tags: post.topic.tags.map(&:name) + ["secret"],
|
|
)
|
|
expect(result).to eq(true)
|
|
}.to_not change { post.topic.reload.bumped_at }
|
|
end
|
|
|
|
it "doesn't bump topic if only staff-only tags are removed" do
|
|
expect {
|
|
result =
|
|
post_revisor.revise!(
|
|
Fabricate(:admin),
|
|
raw: post.raw,
|
|
tags: post.topic.tags.map(&:name) - %w[important secret],
|
|
)
|
|
expect(result).to eq(true)
|
|
}.to_not change { post.topic.reload.bumped_at }
|
|
end
|
|
|
|
it "doesn't bump topic if only staff-only tags are removed and there are no tags left" do
|
|
post.topic.tags = Tag.where(name: %w[important secret]).to_a
|
|
expect {
|
|
result = post_revisor.revise!(Fabricate(:admin), raw: post.raw, tags: [])
|
|
expect(result).to eq(true)
|
|
}.to_not change { post.topic.reload.bumped_at }
|
|
end
|
|
|
|
it "doesn't bump topic if empty string is given" do
|
|
post.topic.tags = Tag.where(name: %w[important secret]).to_a
|
|
expect {
|
|
result = post_revisor.revise!(Fabricate(:admin), raw: post.raw, tags: [""])
|
|
expect(result).to eq(true)
|
|
}.to_not change { post.topic.reload.bumped_at }
|
|
end
|
|
|
|
it "doesn't bump topic if non staff-only tags are added" do
|
|
expect {
|
|
result =
|
|
post_revisor.revise!(
|
|
Fabricate(:admin),
|
|
raw: post.raw,
|
|
tags: post.topic.tags.map(&:name) + [Fabricate(:tag).name],
|
|
)
|
|
expect(result).to eq(true)
|
|
}.not_to change { post.topic.reload.bumped_at }
|
|
end
|
|
end
|
|
end
|
|
|
|
describe "draft cleanup" do
|
|
fab!(:post)
|
|
|
|
it "deletes the draft after successful revision" do
|
|
draft_key = post.topic.draft_key
|
|
Draft.set(post.user, draft_key, 0, '{"reply":"test draft"}')
|
|
|
|
expect(Draft.find_by(user_id: post.user.id, draft_key: draft_key)).to be_present
|
|
|
|
post.revise(post.user, raw: "updated content here for the test")
|
|
|
|
expect(Draft.find_by(user_id: post.user.id, draft_key: draft_key)).to be_nil
|
|
end
|
|
|
|
it "deletes the draft even when draft sequence exceeds DraftSequence" do
|
|
draft_key = post.topic.draft_key
|
|
|
|
# Simulate edge case: draft sequence is higher than DraftSequence
|
|
# When DraftSequence.next! runs, it increments to 6, but sequence < 6 doesn't catch sequence 6
|
|
Draft.create!(user: post.user, draft_key: draft_key, data: '{"reply":"test"}', sequence: 6)
|
|
DraftSequence.create!(user_id: post.user.id, draft_key: draft_key, sequence: 5)
|
|
|
|
expect(Draft.find_by(user_id: post.user.id, draft_key: draft_key)).to be_present
|
|
|
|
post.revise(post.user, raw: "updated content here for the test")
|
|
|
|
expect(Draft.find_by(user_id: post.user.id, draft_key: draft_key)).to be_nil
|
|
end
|
|
end
|
|
|
|
describe "revising reply_to_post_number" do
|
|
fab!(:topic)
|
|
fab!(:op_author, :user)
|
|
fab!(:first_reply_author, :user)
|
|
fab!(:second_reply_author, :user)
|
|
fab!(:editor) { Fabricate(:user, refresh_auto_groups: true) }
|
|
|
|
fab!(:original_post) { Fabricate(:post, topic: topic, user: op_author, post_number: 1) }
|
|
fab!(:first_reply) do
|
|
PostCreator.create!(
|
|
first_reply_author,
|
|
topic_id: topic.id,
|
|
raw: "first reply body, long enough to be valid",
|
|
reply_to_post_number: 1,
|
|
)
|
|
end
|
|
fab!(:second_reply) do
|
|
PostCreator.create!(
|
|
second_reply_author,
|
|
topic_id: topic.id,
|
|
raw: "second reply body, long enough to be valid",
|
|
reply_to_post_number: 1,
|
|
)
|
|
end
|
|
fab!(:post_to_edit) do
|
|
PostCreator.create!(
|
|
editor,
|
|
topic_id: topic.id,
|
|
raw: "my reply body, long enough to be valid",
|
|
reply_to_post_number: 1,
|
|
)
|
|
end
|
|
|
|
def revise(value, opts = {})
|
|
PostRevisor.new(post_to_edit).revise!(
|
|
editor,
|
|
{ reply_to_post_number: value },
|
|
{ bypass_rate_limiter: true }.merge(opts),
|
|
)
|
|
end
|
|
|
|
it "reparents to another earlier post and syncs reply_to_user_id" do
|
|
original_parent_reply_count = original_post.reload.reply_count
|
|
|
|
expect(revise(first_reply.post_number)).to eq(true)
|
|
|
|
post_to_edit.reload
|
|
expect(post_to_edit.reply_to_post_number).to eq(first_reply.post_number)
|
|
expect(post_to_edit.reply_to_user_id).to eq(first_reply.user_id)
|
|
expect(first_reply.reload.reply_count).to eq(1)
|
|
expect(original_post.reload.reply_count).to eq(original_parent_reply_count - 1)
|
|
end
|
|
|
|
it "removes the reply relationship when set to nil" do
|
|
expect(revise(nil)).to eq(true)
|
|
|
|
post_to_edit.reload
|
|
expect(post_to_edit.reply_to_post_number).to be_nil
|
|
expect(post_to_edit.reply_to_user_id).to be_nil
|
|
expect(PostReply.where(post_id: original_post.id, reply_post_id: post_to_edit.id)).to be_empty
|
|
end
|
|
|
|
it "tracks the change in a PostRevision" do
|
|
revise(first_reply.post_number, force_new_version: true)
|
|
|
|
revision = post_to_edit.post_revisions.order(:number).last
|
|
expect(revision.modifications["reply_to_post_number"]).to eq([1, first_reply.post_number])
|
|
end
|
|
|
|
it "does not create a revision when the value is unchanged" do
|
|
expect { revise(post_to_edit.reply_to_post_number) }.not_to change {
|
|
post_to_edit.post_revisions.count
|
|
}
|
|
end
|
|
|
|
it "rejects a self-reference" do
|
|
expect(revise(post_to_edit.post_number)).to eq(false)
|
|
expect(post_to_edit.errors[:reply_to_post_number]).to be_present
|
|
expect(post_to_edit.reload.reply_to_post_number).to eq(1)
|
|
end
|
|
|
|
it "rejects a later post in the topic" do
|
|
later_post =
|
|
PostCreator.create!(
|
|
first_reply_author,
|
|
topic_id: topic.id,
|
|
raw: "a later post, long enough to be valid",
|
|
)
|
|
expect(revise(later_post.post_number)).to eq(false)
|
|
expect(post_to_edit.errors[:reply_to_post_number]).to be_present
|
|
end
|
|
|
|
it "rejects a post that does not exist in the topic" do
|
|
expect(revise(999)).to eq(false)
|
|
expect(post_to_edit.errors[:reply_to_post_number]).to be_present
|
|
end
|
|
|
|
it "rejects a deleted post" do
|
|
first_reply.trash!
|
|
expect(revise(first_reply.post_number)).to eq(false)
|
|
expect(post_to_edit.errors[:reply_to_post_number]).to be_present
|
|
end
|
|
|
|
it "keeps the PostReply row for the old parent if the post still quotes it" do
|
|
post_to_edit.update!(
|
|
raw: "quoting\n[quote=\"#{op_author.username}, post:1, topic:#{topic.id}\"]hi[/quote]",
|
|
)
|
|
post_to_edit.extract_quoted_post_numbers
|
|
post_to_edit.save!
|
|
|
|
expect(revise(first_reply.post_number)).to eq(true)
|
|
expect(
|
|
PostReply.where(post_id: original_post.id, reply_post_id: post_to_edit.id),
|
|
).to be_present
|
|
end
|
|
|
|
it "rejects a target the editor cannot see" do
|
|
SiteSetting.whispers_allowed_groups = Group::AUTO_GROUPS[:staff]
|
|
whisper =
|
|
PostCreator.create!(
|
|
admin,
|
|
topic_id: topic.id,
|
|
raw: "a whisper the editor cannot see",
|
|
post_type: Post.types[:whisper],
|
|
)
|
|
|
|
expect(revise(whisper.post_number)).to eq(false)
|
|
expect(post_to_edit.errors[:reply_to_post_number]).to be_present
|
|
end
|
|
|
|
it "does not create new PostReply rows when the post save fails" do
|
|
SiteSetting.min_post_length = 500
|
|
new_parent_reply_count = first_reply.reload.reply_count
|
|
|
|
result =
|
|
PostRevisor.new(post_to_edit).revise!(
|
|
editor,
|
|
{ raw: "too short", reply_to_post_number: first_reply.post_number },
|
|
bypass_rate_limiter: true,
|
|
)
|
|
|
|
expect(result).to eq(false)
|
|
expect(PostReply.where(post_id: first_reply.id, reply_post_id: post_to_edit.id)).to be_empty
|
|
expect(first_reply.reload.reply_count).to eq(new_parent_reply_count)
|
|
end
|
|
|
|
it "cleans up the PostReply row when the previous parent is already trashed" do
|
|
original_post.trash!
|
|
|
|
expect(
|
|
PostReply.where(post_id: original_post.id, reply_post_id: post_to_edit.id),
|
|
).to be_present
|
|
|
|
expect(revise(first_reply.post_number)).to eq(true)
|
|
|
|
expect(PostReply.where(post_id: original_post.id, reply_post_id: post_to_edit.id)).to be_empty
|
|
end
|
|
|
|
context "with nested reply stats" do
|
|
def direct_reply_count(post)
|
|
NestedViewPostStat.where(post_id: post.id).pick(:direct_reply_count) || 0
|
|
end
|
|
|
|
def total_descendant_count(post)
|
|
NestedViewPostStat.where(post_id: post.id).pick(:total_descendant_count) || 0
|
|
end
|
|
|
|
it "moves the subtree between ancestor chains on reparent" do
|
|
SiteSetting.nested_replies_enabled = true
|
|
|
|
nested_op = Fabricate(:post, topic: topic, user: op_author)
|
|
nested_reparent_target =
|
|
PostCreator.create!(
|
|
first_reply_author,
|
|
topic_id: topic.id,
|
|
raw: "reparent target, long enough to be valid",
|
|
reply_to_post_number: nested_op.post_number,
|
|
)
|
|
moved_post =
|
|
PostCreator.create!(
|
|
editor,
|
|
topic_id: topic.id,
|
|
raw: "the one we'll reparent, long enough",
|
|
reply_to_post_number: nested_op.post_number,
|
|
)
|
|
|
|
# Before: nested_op has two direct children (target + moved_post).
|
|
expect(direct_reply_count(nested_op)).to eq(2)
|
|
expect(total_descendant_count(nested_op)).to eq(2)
|
|
expect(direct_reply_count(nested_reparent_target)).to eq(0)
|
|
expect(total_descendant_count(nested_reparent_target)).to eq(0)
|
|
|
|
result =
|
|
PostRevisor.new(moved_post).revise!(
|
|
editor,
|
|
{ reply_to_post_number: nested_reparent_target.post_number },
|
|
bypass_rate_limiter: true,
|
|
)
|
|
|
|
expect(result).to eq(true)
|
|
# After: moved_post now lives under target. nested_op stays a shared
|
|
# ancestor (2 total descendants) but loses a direct child; target
|
|
# gains one direct + one total.
|
|
expect(direct_reply_count(nested_op)).to eq(1)
|
|
expect(total_descendant_count(nested_op)).to eq(2)
|
|
expect(direct_reply_count(nested_reparent_target)).to eq(1)
|
|
expect(total_descendant_count(nested_reparent_target)).to eq(1)
|
|
end
|
|
|
|
it "moves the subtree up when the new target is the current grandparent" do
|
|
SiteSetting.nested_replies_enabled = true
|
|
|
|
nested_op = Fabricate(:post, topic: topic, user: op_author)
|
|
middle =
|
|
PostCreator.create!(
|
|
first_reply_author,
|
|
topic_id: topic.id,
|
|
raw: "middle, long enough to be valid",
|
|
reply_to_post_number: nested_op.post_number,
|
|
)
|
|
moved_post =
|
|
PostCreator.create!(
|
|
editor,
|
|
topic_id: topic.id,
|
|
raw: "will move up, long enough",
|
|
reply_to_post_number: middle.post_number,
|
|
)
|
|
|
|
expect(direct_reply_count(nested_op)).to eq(1)
|
|
expect(total_descendant_count(nested_op)).to eq(2)
|
|
expect(direct_reply_count(middle)).to eq(1)
|
|
expect(total_descendant_count(middle)).to eq(1)
|
|
|
|
PostRevisor.new(moved_post).revise!(
|
|
editor,
|
|
{ reply_to_post_number: nested_op.post_number },
|
|
bypass_rate_limiter: true,
|
|
)
|
|
|
|
# middle loses its only descendant; nested_op's total stays at 2,
|
|
# its direct child count goes from 1 to 2.
|
|
expect(direct_reply_count(middle)).to eq(0)
|
|
expect(total_descendant_count(middle)).to eq(0)
|
|
expect(direct_reply_count(nested_op)).to eq(2)
|
|
expect(total_descendant_count(nested_op)).to eq(2)
|
|
end
|
|
end
|
|
end
|
|
end
|