discourse-solved/spec/integration/solved_spec.rb

554 lines
18 KiB
Ruby
Raw Normal View History

# frozen_string_literal: true
require "rails_helper"
RSpec.describe "Managing Posts solved status" do
let(:topic) { Fabricate(:topic) }
fab!(:user) { Fabricate(:trust_level_4) }
let(:p1) { Fabricate(:post, topic: topic) }
before { SiteSetting.allow_solved_on_all_topics = true }
describe "customer filters" do
before do
SiteSetting.allow_solved_on_all_topics = false
SiteSetting.enable_solved_tags = solvable_tag.name
end
fab!(:solvable_category) do
category = Fabricate(:category)
CategoryCustomField.create(
category_id: category.id,
name: ::DiscourseSolved::ENABLE_ACCEPTED_ANSWERS_CUSTOM_FIELD,
value: "true",
)
category
end
fab!(:solvable_tag) { Fabricate(:tag) }
fab!(:solved_in_category) do
Fabricate(
:custom_topic,
category: solvable_category,
custom_topic_name: ::DiscourseSolved::ACCEPTED_ANSWER_POST_ID_CUSTOM_FIELD,
value: "42",
)
end
fab!(:solved_in_tag) do
Fabricate(
:custom_topic,
tags: [solvable_tag],
custom_topic_name: ::DiscourseSolved::ACCEPTED_ANSWER_POST_ID_CUSTOM_FIELD,
value: "42",
)
end
fab!(:unsolved_in_category) { Fabricate(:topic, category: solvable_category) }
fab!(:unsolved_in_tag) { Fabricate(:topic, tags: [solvable_tag]) }
fab!(:unsolved_topic) { Fabricate(:topic) }
it "can filter by solved status" do
expect(
TopicsFilter
.new(guardian: Guardian.new)
.filter_from_query_string("status:solved")
.pluck(:id),
).to contain_exactly(solved_in_category.id, solved_in_tag.id)
end
it "can filter by unsolved status" do
expect(
TopicsFilter
.new(guardian: Guardian.new)
.filter_from_query_string("status:unsolved")
.pluck(:id),
).to contain_exactly(unsolved_in_category.id, unsolved_in_tag.id)
end
end
describe "search" do
before { SearchIndexer.enable }
after { SearchIndexer.disable }
it "can prioritize solved topics in search" do
normal_post =
Fabricate(
:post,
raw: "My reply carrot",
topic: Fabricate(:topic, title: "A topic that is not solved but open"),
)
solved_post =
Fabricate(
:post,
raw: "My solution carrot",
topic: Fabricate(:topic, title: "A topic that will be closed", closed: true),
)
DiscourseSolved.accept_answer!(solved_post, Discourse.system_user)
result = Search.execute("carrot")
expect(result.posts.pluck(:id)).to eq([normal_post.id, solved_post.id])
SiteSetting.prioritize_solved_topics_in_search = true
result = Search.execute("carrot")
expect(result.posts.pluck(:id)).to eq([solved_post.id, normal_post.id])
end
describe "#advanced_search" do
fab!(:category_enabled) do
category = Fabricate(:category)
category_custom_field =
CategoryCustomField.new(
category_id: category.id,
name: ::DiscourseSolved::ENABLE_ACCEPTED_ANSWERS_CUSTOM_FIELD,
value: "true",
)
category_custom_field.save
category
end
fab!(:category_disabled) do
category = Fabricate(:category)
category_custom_field =
CategoryCustomField.new(
category_id: category.id,
name: ::DiscourseSolved::ENABLE_ACCEPTED_ANSWERS_CUSTOM_FIELD,
value: "false",
)
category_custom_field.save
category
end
fab!(:tag)
fab!(:topic_unsolved) do
Fabricate(
:custom_topic,
user: user,
category: category_enabled,
custom_topic_name: ::DiscourseSolved::ACCEPTED_ANSWER_POST_ID_CUSTOM_FIELD,
)
end
fab!(:topic_unsolved_2) { Fabricate(:topic, user: user, tags: [tag]) }
fab!(:topic_solved) do
Fabricate(
:custom_topic,
user: user,
category: category_enabled,
custom_topic_name: ::DiscourseSolved::ACCEPTED_ANSWER_POST_ID_CUSTOM_FIELD,
)
end
fab!(:topic_disabled_1) do
Fabricate(
:custom_topic,
user: user,
category: category_disabled,
custom_topic_name: ::DiscourseSolved::ACCEPTED_ANSWER_POST_ID_CUSTOM_FIELD,
)
end
fab!(:topic_disabled_2) do
Fabricate(
:custom_topic,
user: user,
category: category_disabled,
custom_topic_name: "another_custom_field",
)
end
fab!(:post_unsolved) { Fabricate(:post, topic: topic_unsolved) }
fab!(:post_unsolved_2) { Fabricate(:post, topic: topic_unsolved_2) }
fab!(:post_solved) do
post = Fabricate(:post, topic: topic_solved)
DiscourseSolved.accept_answer!(post, Discourse.system_user)
post
end
fab!(:post_disabled_1) { Fabricate(:post, topic: topic_disabled_1) }
fab!(:post_disabled_2) { Fabricate(:post, topic: topic_disabled_2) }
before do
SiteSetting.enable_solved_tags = tag.name
SearchIndexer.enable
Jobs.run_immediately!
SearchIndexer.index(topic_unsolved, force: true)
SearchIndexer.index(topic_unsolved_2, force: true)
SearchIndexer.index(topic_solved, force: true)
SearchIndexer.index(topic_disabled_1, force: true)
SearchIndexer.index(topic_disabled_2, force: true)
end
after { SearchIndexer.disable }
describe "searches for unsolved topics" do
describe "when allow solved on all topics is disabled" do
before { SiteSetting.allow_solved_on_all_topics = false }
it "only returns unsolved posts from categories and tags where solving is enabled" do
result = Search.execute("status:unsolved")
expect(result.posts.pluck(:id)).to match_array([post_unsolved.id, post_unsolved_2.id])
end
it "returns the filtered results when combining search with a tag" do
result = Search.execute("status:unsolved tag:#{tag.name}")
expect(result.posts.pluck(:id)).to match_array([post_unsolved_2.id])
end
end
describe "when allow solved on all topics is enabled" do
before { SiteSetting.allow_solved_on_all_topics = true }
it "only returns posts where the post is not solved" do
result = Search.execute("status:unsolved")
expect(result.posts.pluck(:id)).to match_array(
[post_unsolved.id, post_unsolved_2.id, post_disabled_1.id, post_disabled_2.id],
)
end
end
end
end
end
describe "auto bump" do
it "does not automatically bump solved topics" do
category = Fabricate(:category_with_definition)
post = create_post(category: category)
post2 = create_post(category: category)
DiscourseSolved.accept_answer!(post, Discourse.system_user)
category.num_auto_bump_daily = 2
category.save!
freeze_time 1.month.from_now
expect(category.auto_bump_topic!).to eq(true)
freeze_time 13.hours.from_now
expect(category.auto_bump_topic!).to eq(false)
expect(post.topic.reload.posts_count).to eq(1)
expect(post2.topic.reload.posts_count).to eq(2)
end
end
describe "accepting a post as the answer" do
before do
sign_in(user)
SiteSetting.solved_topics_auto_close_hours = 2
end
it "can mark a post as the accepted answer correctly" do
freeze_time
2017-08-31 22:42:31 -04:00
post "/solution/accept.json", params: { id: p1.id }
expect(response.status).to eq(200)
expect(p1.reload.custom_fields["is_accepted_answer"]).to eq("true")
topic.reload
expect(topic.public_topic_timer.status_type).to eq(TopicTimer.types[:silent_close])
expect(topic.custom_fields["solved_auto_close_topic_timer_id"].to_i).to eq(
topic.public_topic_timer.id,
)
expect(topic.public_topic_timer.execute_at).to eq_time(Time.zone.now + 2.hours)
expect(topic.public_topic_timer.based_on_last_post).to eq(true)
end
it "gives priority to category's solved_topics_auto_close_hours setting" do
freeze_time
custom_auto_close_category = Fabricate(:category)
topic_2 = Fabricate(:topic, category: custom_auto_close_category)
post_2 = Fabricate(:post, topic: topic_2)
custom_auto_close_category.custom_fields["solved_topics_auto_close_hours"] = 4
custom_auto_close_category.save_custom_fields
post "/solution/accept.json", params: { id: post_2.id }
expect(response.status).to eq(200)
expect(post_2.reload.custom_fields["is_accepted_answer"]).to eq("true")
topic_2.reload
expect(topic_2.public_topic_timer.status_type).to eq(TopicTimer.types[:silent_close])
expect(topic_2.custom_fields["solved_auto_close_topic_timer_id"].to_i).to eq(
topic_2.public_topic_timer.id,
)
expect(topic_2.public_topic_timer.execute_at).to eq_time(Time.zone.now + 4.hours)
expect(topic_2.public_topic_timer.based_on_last_post).to eq(true)
end
it "sends notifications to correct users" do
SiteSetting.notify_on_staff_accept_solved = true
user = Fabricate(:user)
topic = Fabricate(:topic, user: user)
post = Fabricate(:post, post_number: 2, topic: topic)
op = topic.user
user = post.user
expect { DiscourseSolved.accept_answer!(post, Discourse.system_user) }.to change {
user.notifications.count
}.by(1) & change { op.notifications.count }.by(1)
notification = user.notifications.last
expect(notification.notification_type).to eq(Notification.types[:custom])
expect(notification.topic_id).to eq(post.topic_id)
expect(notification.post_number).to eq(post.post_number)
notification = op.notifications.last
expect(notification.notification_type).to eq(Notification.types[:custom])
expect(notification.topic_id).to eq(post.topic_id)
expect(notification.post_number).to eq(post.post_number)
end
it "does not set a timer when the topic is closed" do
2017-07-18 15:24:09 -04:00
topic.update!(closed: true)
2017-08-31 22:42:31 -04:00
post "/solution/accept.json", params: { id: p1.id }
expect(response.status).to eq(200)
2017-07-18 15:24:09 -04:00
p1.reload
topic.reload
2017-07-18 15:24:09 -04:00
expect(p1.custom_fields["is_accepted_answer"]).to eq("true")
expect(topic.public_topic_timer).to eq(nil)
expect(topic.closed).to eq(true)
end
it "works with staff and trashed topics" do
topic.trash!(Discourse.system_user)
post "/solution/accept.json", params: { id: p1.id }
expect(response.status).to eq(403)
sign_in(Fabricate(:admin))
post "/solution/accept.json", params: { id: p1.id }
expect(response.status).to eq(200)
p1.reload
expect(p1.custom_fields["is_accepted_answer"]).to eq("true")
end
it "removes the solution when the post is deleted" do
reply = Fabricate(:post, post_number: 2, topic: topic)
post "/solution/accept.json", params: { id: reply.id }
expect(response.status).to eq(200)
reply.reload
expect(reply.custom_fields["is_accepted_answer"]).to eq("true")
expect(reply.topic.custom_fields["accepted_answer_post_id"].to_i).to eq(reply.id)
PostDestroyer.new(Discourse.system_user, reply).destroy
reply.reload
expect(reply.custom_fields["is_accepted_answer"]).to eq(nil)
expect(reply.topic.custom_fields["accepted_answer_post_id"]).to eq(nil)
end
it "does not allow you to accept a whisper" do
whisper = Fabricate(:post, topic: topic, post_type: Post.types[:whisper])
sign_in(Fabricate(:admin))
post "/solution/accept.json", params: { id: whisper.id }
expect(response.status).to eq(403)
end
FEATURE: Publish WebHook event when solving/unsolving (#85) * FEATURE: Publish WebHook event when solving/unsolving This feature will publish a post edit webhook event whenever a solution is accepted or unaccepted. I went ahead and used the existing post-edit webhook because all the post custom fields for the solved plugin are already included in the post-edit serializer. * Create Solved Event Webhook This commit adds a solved event webhook that will only trigger when an answer has been marked as accepted or unaccepted. It uses 100 as the webhook ID. This way any new webhooks in core can keep using lower numbers like 11, 12, 13, but plugins can use 101, 102, etc. * Removed functionality that was added to core This [PR][1] to discourse core adds what what removed in this commit. It is better to have this logic in core so that it is discoverable and future webhooks won't end up accidentally using the same ID. [1]: https://github.com/discourse/discourse/pull/9110 * UX: Add "solved" status filter in advanced search page. And rename `in:solved` to `status:solved`. * FEATURE: Publish WebHook event when solving/unsolving This feature will publish a post edit webhook event whenever a solution is accepted or unaccepted. I went ahead and used the existing post-edit webhook because all the post custom fields for the solved plugin are already included in the post-edit serializer. * Create Solved Event Webhook This commit adds a solved event webhook that will only trigger when an answer has been marked as accepted or unaccepted. It uses 100 as the webhook ID. This way any new webhooks in core can keep using lower numbers like 11, 12, 13, but plugins can use 101, 102, etc. * Removed functionality that was added to core This [PR][1] to discourse core adds what what removed in this commit. It is better to have this logic in core so that it is discoverable and future webhooks won't end up accidentally using the same ID. [1]: https://github.com/discourse/discourse/pull/9110 Co-authored-by: Vinoth Kannan <vinothkannan@vinkas.com>
2020-03-06 13:28:29 -05:00
it "triggers a webhook" do
FEATURE: Publish WebHook event when solving/unsolving (#85) * FEATURE: Publish WebHook event when solving/unsolving This feature will publish a post edit webhook event whenever a solution is accepted or unaccepted. I went ahead and used the existing post-edit webhook because all the post custom fields for the solved plugin are already included in the post-edit serializer. * Create Solved Event Webhook This commit adds a solved event webhook that will only trigger when an answer has been marked as accepted or unaccepted. It uses 100 as the webhook ID. This way any new webhooks in core can keep using lower numbers like 11, 12, 13, but plugins can use 101, 102, etc. * Removed functionality that was added to core This [PR][1] to discourse core adds what what removed in this commit. It is better to have this logic in core so that it is discoverable and future webhooks won't end up accidentally using the same ID. [1]: https://github.com/discourse/discourse/pull/9110 * UX: Add "solved" status filter in advanced search page. And rename `in:solved` to `status:solved`. * FEATURE: Publish WebHook event when solving/unsolving This feature will publish a post edit webhook event whenever a solution is accepted or unaccepted. I went ahead and used the existing post-edit webhook because all the post custom fields for the solved plugin are already included in the post-edit serializer. * Create Solved Event Webhook This commit adds a solved event webhook that will only trigger when an answer has been marked as accepted or unaccepted. It uses 100 as the webhook ID. This way any new webhooks in core can keep using lower numbers like 11, 12, 13, but plugins can use 101, 102, etc. * Removed functionality that was added to core This [PR][1] to discourse core adds what what removed in this commit. It is better to have this logic in core so that it is discoverable and future webhooks won't end up accidentally using the same ID. [1]: https://github.com/discourse/discourse/pull/9110 Co-authored-by: Vinoth Kannan <vinothkannan@vinkas.com>
2020-03-06 13:28:29 -05:00
Fabricate(:solved_web_hook)
post "/solution/accept.json", params: { id: p1.id }
job_args = Jobs::EmitWebHookEvent.jobs[0]["args"].first
expect(job_args["event_name"]).to eq("accepted_solution")
payload = JSON.parse(job_args["payload"])
expect(payload["id"]).to eq(p1.id)
end
end
describe "#unaccept" do
before { sign_in(user) }
describe "when solved_topics_auto_close_hours is enabled" do
before do
SiteSetting.solved_topics_auto_close_hours = 2
DiscourseSolved.accept_answer!(p1, user)
end
it "should unmark the post as solved" do
expect do post "/solution/unaccept.json", params: { id: p1.id } end.to change {
topic.reload.public_topic_timer
}.to(nil)
expect(response.status).to eq(200)
p1.reload
expect(p1.custom_fields["is_accepted_answer"]).to eq(nil)
expect(p1.topic.custom_fields["accepted_answer_post_id"]).to eq(nil)
end
FEATURE: Publish WebHook event when solving/unsolving (#85) * FEATURE: Publish WebHook event when solving/unsolving This feature will publish a post edit webhook event whenever a solution is accepted or unaccepted. I went ahead and used the existing post-edit webhook because all the post custom fields for the solved plugin are already included in the post-edit serializer. * Create Solved Event Webhook This commit adds a solved event webhook that will only trigger when an answer has been marked as accepted or unaccepted. It uses 100 as the webhook ID. This way any new webhooks in core can keep using lower numbers like 11, 12, 13, but plugins can use 101, 102, etc. * Removed functionality that was added to core This [PR][1] to discourse core adds what what removed in this commit. It is better to have this logic in core so that it is discoverable and future webhooks won't end up accidentally using the same ID. [1]: https://github.com/discourse/discourse/pull/9110 * UX: Add "solved" status filter in advanced search page. And rename `in:solved` to `status:solved`. * FEATURE: Publish WebHook event when solving/unsolving This feature will publish a post edit webhook event whenever a solution is accepted or unaccepted. I went ahead and used the existing post-edit webhook because all the post custom fields for the solved plugin are already included in the post-edit serializer. * Create Solved Event Webhook This commit adds a solved event webhook that will only trigger when an answer has been marked as accepted or unaccepted. It uses 100 as the webhook ID. This way any new webhooks in core can keep using lower numbers like 11, 12, 13, but plugins can use 101, 102, etc. * Removed functionality that was added to core This [PR][1] to discourse core adds what what removed in this commit. It is better to have this logic in core so that it is discoverable and future webhooks won't end up accidentally using the same ID. [1]: https://github.com/discourse/discourse/pull/9110 Co-authored-by: Vinoth Kannan <vinothkannan@vinkas.com>
2020-03-06 13:28:29 -05:00
end
it "triggers a webhook" do
FEATURE: Publish WebHook event when solving/unsolving (#85) * FEATURE: Publish WebHook event when solving/unsolving This feature will publish a post edit webhook event whenever a solution is accepted or unaccepted. I went ahead and used the existing post-edit webhook because all the post custom fields for the solved plugin are already included in the post-edit serializer. * Create Solved Event Webhook This commit adds a solved event webhook that will only trigger when an answer has been marked as accepted or unaccepted. It uses 100 as the webhook ID. This way any new webhooks in core can keep using lower numbers like 11, 12, 13, but plugins can use 101, 102, etc. * Removed functionality that was added to core This [PR][1] to discourse core adds what what removed in this commit. It is better to have this logic in core so that it is discoverable and future webhooks won't end up accidentally using the same ID. [1]: https://github.com/discourse/discourse/pull/9110 * UX: Add "solved" status filter in advanced search page. And rename `in:solved` to `status:solved`. * FEATURE: Publish WebHook event when solving/unsolving This feature will publish a post edit webhook event whenever a solution is accepted or unaccepted. I went ahead and used the existing post-edit webhook because all the post custom fields for the solved plugin are already included in the post-edit serializer. * Create Solved Event Webhook This commit adds a solved event webhook that will only trigger when an answer has been marked as accepted or unaccepted. It uses 100 as the webhook ID. This way any new webhooks in core can keep using lower numbers like 11, 12, 13, but plugins can use 101, 102, etc. * Removed functionality that was added to core This [PR][1] to discourse core adds what what removed in this commit. It is better to have this logic in core so that it is discoverable and future webhooks won't end up accidentally using the same ID. [1]: https://github.com/discourse/discourse/pull/9110 Co-authored-by: Vinoth Kannan <vinothkannan@vinkas.com>
2020-03-06 13:28:29 -05:00
Fabricate(:solved_web_hook)
post "/solution/unaccept.json", params: { id: p1.id }
job_args = Jobs::EmitWebHookEvent.jobs[0]["args"].first
expect(job_args["event_name"]).to eq("unaccepted_solution")
payload = JSON.parse(job_args["payload"])
expect(payload["id"]).to eq(p1.id)
end
end
context "with group moderators" do
2024-03-05 06:14:32 -05:00
fab!(:group_user)
let(:user_gm) { group_user.user }
let(:group) { group_user.group }
before do
SiteSetting.enable_category_group_moderation = true
p1.topic.category.update!(reviewable_by_group_id: group.id)
sign_in(user_gm)
end
it "can accept a solution" do
post "/solution/accept.json", params: { id: p1.id }
expect(response.status).to eq(200)
end
end
context "with discourse-assign installed", if: defined?(DiscourseAssign) do
let(:admin) { Fabricate(:admin) }
fab!(:group)
before do
SiteSetting.solved_enabled = true
SiteSetting.assign_enabled = true
SiteSetting.enable_assign_status = true
SiteSetting.assign_allowed_on_groups = "#{group.id}"
SiteSetting.assigns_public = true
SiteSetting.assignment_status_on_solve = "Done"
SiteSetting.assignment_status_on_unsolve = "New"
SiteSetting.ignore_solved_topics_in_assigned_reminder = false
group.add(p1.acting_user)
group.add(user)
sign_in(user)
end
describe "updating assignment status on solve when assignment_status_on_solve is set" do
it "update all assignments to this status when a post is accepted" do
assigner = Assigner.new(p1.topic, user)
result = assigner.assign(user)
expect(result[:success]).to eq(true)
expect(p1.topic.assignment.status).to eq("New")
DiscourseSolved.accept_answer!(p1, user)
expect(p1.reload.custom_fields["is_accepted_answer"]).to eq("true")
expect(p1.topic.assignment.reload.status).to eq("Done")
end
it "update all assignments to this status when a post is unaccepted" do
assigner = Assigner.new(p1.topic, user)
result = assigner.assign(user)
expect(result[:success]).to eq(true)
DiscourseSolved.accept_answer!(p1, user)
expect(p1.reload.topic.assignment.reload.status).to eq("Done")
DiscourseSolved.unaccept_answer!(p1)
expect(p1.reload.custom_fields["is_accepted_answer"]).to eq(nil)
expect(p1.reload.topic.assignment.reload.status).to eq("New")
end
it "does not update the assignee when a post is accepted" do
user_1 = Fabricate(:user)
user_2 = Fabricate(:user)
user_3 = Fabricate(:user)
group.add(user_1)
group.add(user_2)
group.add(user_3)
topic_question = Fabricate(:topic, user: user_1)
Fabricate(:post, topic: topic_question, user: user_1)
Fabricate(:post, topic: topic_question, user: user_2)
result = Assigner.new(topic_question, user_2).assign(user_2)
expect(result[:success]).to eq(true)
post_response = Fabricate(:post, topic: topic_question, user: user_3)
Assigner.new(post_response, user_3).assign(user_3)
DiscourseSolved.accept_answer!(post_response, user_1)
expect(topic_question.assignment.assigned_to_id).to eq(user_2.id)
expect(post_response.assignment.assigned_to_id).to eq(user_3.id)
DiscourseSolved.unaccept_answer!(post_response)
expect(topic_question.assignment.assigned_to_id).to eq(user_2.id)
expect(post_response.assignment.assigned_to_id).to eq(user_3.id)
end
describe "assigned topic reminder" do
it "excludes solved topics when ignore_solved_topics_in_assigned_reminder is false" do
other_topic = Fabricate(:topic, title: "Topic that should be there")
post = Fabricate(:post, topic: other_topic, user: user)
other_topic2 = Fabricate(:topic, title: "Topic that should be there2")
post2 = Fabricate(:post, topic: other_topic2, user: user)
Assigner.new(post.topic, user).assign(user)
Assigner.new(post2.topic, user).assign(user)
reminder = PendingAssignsReminder.new
topics = reminder.send(:assigned_topics, user, order: :asc)
expect(topics.to_a.length).to eq(2)
DiscourseSolved.accept_answer!(post2, Discourse.system_user)
topics = reminder.send(:assigned_topics, user, order: :asc)
expect(topics.to_a.length).to eq(2)
expect(topics).to include(other_topic2)
SiteSetting.ignore_solved_topics_in_assigned_reminder = true
topics = reminder.send(:assigned_topics, user, order: :asc)
expect(topics.to_a.length).to eq(1)
expect(topics).not_to include(other_topic2)
expect(topics).to include(other_topic)
end
end
end
end
describe "#unaccept_answer!" do
it "works even when the topic has been deleted" do
user = Fabricate(:user, trust_level: 1)
topic = Fabricate(:topic, user:)
reply = Fabricate(:post, topic:, user:, post_number: 2)
DiscourseSolved.accept_answer!(reply, user)
topic.trash!(Discourse.system_user)
reply.reload
expect(reply.topic).to eq(nil)
expect { DiscourseSolved.unaccept_answer!(reply) }.not_to raise_error
end
end
end