mirror of
https://github.com/discourse/discourse.git
synced 2024-11-26 04:33:37 +08:00
c3bab3ef38
It used to change the category of the topic, instead of the destination category (topic.category_id instead of topic.shared_draft.category_id). The shared drafts controls were displayed only if the current category matched the 'shared drafts category', which was not true for shared drafts that had their categories changed (affected by the previous bug).
3769 lines
118 KiB
Ruby
3769 lines
118 KiB
Ruby
# coding: utf-8
|
||
# frozen_string_literal: true
|
||
|
||
require 'rails_helper'
|
||
|
||
RSpec.describe TopicsController do
|
||
fab!(:topic) { Fabricate(:topic) }
|
||
fab!(:user) { Fabricate(:user) }
|
||
fab!(:moderator) { Fabricate(:moderator) }
|
||
fab!(:admin) { Fabricate(:admin) }
|
||
fab!(:trust_level_4) { Fabricate(:trust_level_4) }
|
||
|
||
describe '#wordpress' do
|
||
let!(:user) { sign_in(moderator) }
|
||
let(:p1) { Fabricate(:post, user: moderator) }
|
||
let(:topic) { p1.topic }
|
||
let!(:p2) { Fabricate(:post, topic: topic, user: moderator) }
|
||
|
||
it "returns the JSON in the format our wordpress plugin needs" do
|
||
SiteSetting.external_system_avatars_enabled = false
|
||
|
||
get "/t/#{topic.id}/wordpress.json", params: { best: 3 }
|
||
|
||
expect(response.status).to eq(200)
|
||
json = response.parsed_body
|
||
|
||
# The JSON has the data the wordpress plugin needs
|
||
expect(json['id']).to eq(topic.id)
|
||
expect(json['posts_count']).to eq(2)
|
||
expect(json['filtered_posts_count']).to eq(2)
|
||
|
||
# Posts
|
||
expect(json['posts'].size).to eq(1)
|
||
post = json['posts'][0]
|
||
expect(post['id']).to eq(p2.id)
|
||
expect(post['username']).to eq(user.username)
|
||
expect(post['avatar_template']).to eq("#{Discourse.base_url_no_prefix}#{user.avatar_template}")
|
||
expect(post['name']).to eq(user.name)
|
||
expect(post['created_at']).to be_present
|
||
expect(post['cooked']).to eq(p2.cooked)
|
||
|
||
# Participants
|
||
expect(json['participants'].size).to eq(1)
|
||
participant = json['participants'][0]
|
||
expect(participant['id']).to eq(user.id)
|
||
expect(participant['username']).to eq(user.username)
|
||
expect(participant['avatar_template']).to eq("#{Discourse.base_url_no_prefix}#{user.avatar_template}")
|
||
end
|
||
end
|
||
|
||
describe '#move_posts' do
|
||
before do
|
||
SiteSetting.min_topic_title_length = 2
|
||
SiteSetting.tagging_enabled = true
|
||
end
|
||
|
||
it 'needs you to be logged in' do
|
||
post "/t/111/move-posts.json", params: {
|
||
title: 'blah',
|
||
post_ids: [1, 2, 3]
|
||
}
|
||
expect(response.status).to eq(403)
|
||
end
|
||
|
||
describe 'moving to a new topic' do
|
||
let(:p1) { Fabricate(:post, user: user, post_number: 1) }
|
||
let(:p2) { Fabricate(:post, user: user, post_number: 2, topic: p1.topic) }
|
||
let!(:topic) { p1.topic }
|
||
|
||
it "raises an error without post_ids" do
|
||
sign_in(moderator)
|
||
post "/t/#{topic.id}/move-posts.json", params: { title: 'blah' }
|
||
expect(response.status).to eq(400)
|
||
end
|
||
|
||
it "raises an error when the user doesn't have permission to move the posts" do
|
||
sign_in(user)
|
||
|
||
post "/t/#{topic.id}/move-posts.json", params: {
|
||
title: 'blah', post_ids: [p1.post_number, p2.post_number]
|
||
}
|
||
|
||
expect(response).to be_forbidden
|
||
end
|
||
|
||
it "raises an error when the OP is not a regular post" do
|
||
sign_in(moderator)
|
||
p2 = Fabricate(:post, topic: topic, post_number: 2, post_type: Post.types[:whisper])
|
||
p3 = Fabricate(:post, topic: topic, post_number: 3)
|
||
|
||
post "/t/#{topic.id}/move-posts.json", params: {
|
||
title: 'blah', post_ids: [p2.id, p3.id]
|
||
}
|
||
expect(response.status).to eq(422)
|
||
|
||
result = response.parsed_body
|
||
|
||
expect(result['errors']).to be_present
|
||
end
|
||
|
||
context 'success' do
|
||
fab!(:category) { Fabricate(:category) }
|
||
|
||
before { sign_in(admin) }
|
||
|
||
it "returns success" do
|
||
expect do
|
||
post "/t/#{topic.id}/move-posts.json", params: {
|
||
title: 'Logan is a good movie',
|
||
post_ids: [p2.id],
|
||
category_id: category.id,
|
||
tags: ["tag1", "tag2"]
|
||
}
|
||
end.to change { Topic.count }.by(1)
|
||
|
||
expect(response.status).to eq(200)
|
||
|
||
result = response.parsed_body
|
||
|
||
expect(result['success']).to eq(true)
|
||
|
||
new_topic = Topic.last
|
||
expect(result['url']).to eq(new_topic.relative_url)
|
||
expect(new_topic.excerpt).to eq(p2.excerpt_for_topic)
|
||
expect(Tag.all.pluck(:name)).to contain_exactly("tag1", "tag2")
|
||
end
|
||
|
||
describe 'when topic has been deleted' do
|
||
it 'should still be able to move posts' do
|
||
PostDestroyer.new(admin, topic.first_post).destroy
|
||
|
||
expect(topic.reload.deleted_at).to_not be_nil
|
||
|
||
expect do
|
||
post "/t/#{topic.id}/move-posts.json", params: {
|
||
title: 'Logan is a good movie',
|
||
post_ids: [p2.id],
|
||
category_id: category.id
|
||
}
|
||
end.to change { Topic.count }.by(1)
|
||
|
||
expect(response.status).to eq(200)
|
||
|
||
result = response.parsed_body
|
||
|
||
expect(result['success']).to eq(true)
|
||
expect(result['url']).to eq(Topic.last.relative_url)
|
||
end
|
||
end
|
||
end
|
||
|
||
context 'failure' do
|
||
it "returns JSON with a false success" do
|
||
sign_in(moderator)
|
||
post "/t/#{topic.id}/move-posts.json", params: {
|
||
post_ids: [p2.id]
|
||
}
|
||
expect(response.status).to eq(200)
|
||
result = response.parsed_body
|
||
expect(result['success']).to eq(false)
|
||
expect(result['url']).to be_blank
|
||
end
|
||
end
|
||
|
||
describe "moving replied posts" do
|
||
context 'success' do
|
||
it "moves the child posts too" do
|
||
user = sign_in(moderator)
|
||
p1 = Fabricate(:post, topic: topic, user: user)
|
||
p2 = Fabricate(:post, topic: topic, user: user, reply_to_post_number: p1.post_number)
|
||
PostReply.create(post_id: p1.id, reply_post_id: p2.id)
|
||
|
||
post "/t/#{topic.id}/move-posts.json", params: {
|
||
title: 'new topic title',
|
||
post_ids: [p1.id],
|
||
reply_post_ids: [p1.id]
|
||
}
|
||
expect(response.status).to eq(200)
|
||
|
||
p1.reload
|
||
p2.reload
|
||
|
||
new_topic_id = response.parsed_body["url"].split("/").last.to_i
|
||
new_topic = Topic.find(new_topic_id)
|
||
expect(p1.topic.id).to eq(new_topic.id)
|
||
expect(p2.topic.id).to eq(new_topic.id)
|
||
expect(p2.reply_to_post_number).to eq(p1.post_number)
|
||
end
|
||
end
|
||
end
|
||
end
|
||
|
||
describe "moving to a new topic as a group moderator" do
|
||
fab!(:group_user) { Fabricate(:group_user) }
|
||
fab!(:category) { Fabricate(:category, reviewable_by_group: group_user.group) }
|
||
fab!(:topic) { Fabricate(:topic, category: category) }
|
||
fab!(:p1) { Fabricate(:post, user: group_user.user, post_number: 1, topic: topic) }
|
||
fab!(:p2) { Fabricate(:post, user: group_user.user, post_number: 2, topic: topic) }
|
||
let(:user) { group_user.user }
|
||
|
||
before do
|
||
sign_in(user)
|
||
SiteSetting.enable_category_group_moderation = true
|
||
end
|
||
|
||
it "moves the posts" do
|
||
expect do
|
||
post "/t/#{topic.id}/move-posts.json", params: {
|
||
title: 'Logan is a good movie',
|
||
post_ids: [p2.id],
|
||
category_id: category.id
|
||
}
|
||
end.to change { Topic.count }.by(1)
|
||
|
||
expect(response.status).to eq(200)
|
||
result = response.parsed_body
|
||
expect(result['success']).to eq(true)
|
||
expect(result['url']).to eq(Topic.last.relative_url)
|
||
end
|
||
|
||
it "does not allow posts to be moved to a private category" do
|
||
staff_category = Fabricate(:category)
|
||
staff_category.set_permissions(staff: :full)
|
||
staff_category.save!
|
||
|
||
post "/t/#{topic.id}/move-posts.json", params: {
|
||
title: 'Logan is a good movie',
|
||
post_ids: [p2.id],
|
||
category_id: staff_category.id
|
||
}
|
||
|
||
expect(response).to be_forbidden
|
||
end
|
||
|
||
it "does not allow posts outside of the category to be moved" do
|
||
topic.update!(category: nil)
|
||
|
||
post "/t/#{topic.id}/move-posts.json", params: {
|
||
title: 'blah', post_ids: [p1.post_number, p2.post_number]
|
||
}
|
||
|
||
expect(response).to be_forbidden
|
||
end
|
||
end
|
||
|
||
describe 'moving to an existing topic' do
|
||
let!(:user) { sign_in(moderator) }
|
||
let(:p1) { Fabricate(:post, user: user) }
|
||
let(:topic) { p1.topic }
|
||
fab!(:dest_topic) { Fabricate(:topic) }
|
||
let(:p2) { Fabricate(:post, user: user, topic: topic) }
|
||
|
||
context 'success' do
|
||
it "returns success" do
|
||
user
|
||
post "/t/#{topic.id}/move-posts.json", params: {
|
||
post_ids: [p2.id],
|
||
destination_topic_id: dest_topic.id
|
||
}
|
||
|
||
expect(response.status).to eq(200)
|
||
result = response.parsed_body
|
||
expect(result['success']).to eq(true)
|
||
expect(result['url']).to be_present
|
||
end
|
||
|
||
it "triggers an event on merge" do
|
||
begin
|
||
called = false
|
||
|
||
assert = -> (original_topic, destination_topic) do
|
||
called = true
|
||
expect(original_topic).to eq(topic)
|
||
expect(destination_topic).to eq(dest_topic)
|
||
end
|
||
|
||
DiscourseEvent.on(:topic_merged, &assert)
|
||
|
||
post "/t/#{topic.id}/move-posts.json", params: {
|
||
post_ids: [p2.id],
|
||
destination_topic_id: dest_topic.id
|
||
}
|
||
|
||
expect(called).to eq(true)
|
||
expect(response.status).to eq(200)
|
||
ensure
|
||
DiscourseEvent.off(:topic_merged, &assert)
|
||
end
|
||
end
|
||
end
|
||
|
||
context 'failure' do
|
||
let(:p2) { Fabricate(:post, user: user) }
|
||
it "returns JSON with a false success" do
|
||
post "/t/#{topic.id}/move-posts.json", params: {
|
||
post_ids: [p2.id]
|
||
}
|
||
|
||
expect(response.status).to eq(200)
|
||
result = response.parsed_body
|
||
expect(result['success']).to eq(false)
|
||
expect(result['url']).to be_blank
|
||
end
|
||
end
|
||
end
|
||
|
||
describe "moving to an existing topic as a group moderator" do
|
||
fab!(:group_user) { Fabricate(:group_user) }
|
||
fab!(:category) { Fabricate(:category, reviewable_by_group: group_user.group) }
|
||
fab!(:topic) { Fabricate(:topic, category: category) }
|
||
fab!(:p1) { Fabricate(:post, user: group_user.user, post_number: 1, topic: topic) }
|
||
fab!(:p2) { Fabricate(:post, user: group_user.user, post_number: 2, topic: topic) }
|
||
fab!(:dest_topic) { Fabricate(:topic) }
|
||
|
||
let(:user) { group_user.user }
|
||
|
||
before do
|
||
sign_in(user)
|
||
SiteSetting.enable_category_group_moderation = true
|
||
end
|
||
|
||
it "moves the posts" do
|
||
post "/t/#{topic.id}/move-posts.json", params: {
|
||
post_ids: [p2.id],
|
||
destination_topic_id: dest_topic.id
|
||
}
|
||
|
||
expect(response.status).to eq(200)
|
||
result = response.parsed_body
|
||
expect(result['success']).to eq(true)
|
||
expect(result['url']).to be_present
|
||
end
|
||
|
||
it "does not allow posts to be moved to a private category" do
|
||
staff_category = Fabricate(:category)
|
||
staff_category.set_permissions(staff: :full)
|
||
staff_category.save!
|
||
dest_topic.update!(category: staff_category)
|
||
|
||
post "/t/#{topic.id}/move-posts.json", params: {
|
||
post_ids: [p2.id],
|
||
destination_topic_id: dest_topic.id
|
||
}
|
||
|
||
expect(response).to be_forbidden
|
||
end
|
||
|
||
it "does not allow posts outside of the category to be moved" do
|
||
topic.update!(category: nil)
|
||
|
||
post "/t/#{topic.id}/move-posts.json", params: {
|
||
post_ids: [p1.post_number, p2.post_number],
|
||
destination_topic_id: dest_topic.id
|
||
}
|
||
|
||
expect(response).to be_forbidden
|
||
end
|
||
end
|
||
|
||
describe 'moving to a new message' do
|
||
let!(:message) { Fabricate(:private_message_topic) }
|
||
let!(:p1) { Fabricate(:post, user: user, post_number: 1, topic: message) }
|
||
let!(:p2) { Fabricate(:post, user: user, post_number: 2, topic: message) }
|
||
|
||
it "raises an error without post_ids" do
|
||
sign_in(moderator)
|
||
post "/t/#{message.id}/move-posts.json", params: { title: 'blah', archetype: 'private_message' }
|
||
expect(response.status).to eq(400)
|
||
end
|
||
|
||
it "raises an error when the user doesn't have permission to move the posts" do
|
||
sign_in(trust_level_4)
|
||
|
||
post "/t/#{message.id}/move-posts.json", params: {
|
||
title: 'blah', post_ids: [p1.post_number, p2.post_number], archetype: 'private_message'
|
||
}
|
||
|
||
expect(response.status).to eq(403)
|
||
result = response.parsed_body
|
||
expect(result['errors']).to be_present
|
||
end
|
||
|
||
context 'success' do
|
||
before { sign_in(admin) }
|
||
|
||
it "returns success" do
|
||
SiteSetting.allow_staff_to_tag_pms = true
|
||
|
||
expect do
|
||
post "/t/#{message.id}/move-posts.json", params: {
|
||
title: 'Logan is a good movie',
|
||
post_ids: [p2.id],
|
||
archetype: 'private_message',
|
||
tags: ["tag1", "tag2"]
|
||
}
|
||
end.to change { Topic.count }.by(1)
|
||
|
||
expect(response.status).to eq(200)
|
||
|
||
result = response.parsed_body
|
||
|
||
expect(result['success']).to eq(true)
|
||
expect(result['url']).to eq(Topic.last.relative_url)
|
||
expect(Tag.all.pluck(:name)).to contain_exactly("tag1", "tag2")
|
||
end
|
||
|
||
describe 'when message has been deleted' do
|
||
it 'should still be able to move posts' do
|
||
PostDestroyer.new(admin, message.first_post).destroy
|
||
|
||
expect(message.reload.deleted_at).to_not be_nil
|
||
|
||
expect do
|
||
post "/t/#{message.id}/move-posts.json", params: {
|
||
title: 'Logan is a good movie',
|
||
post_ids: [p2.id],
|
||
archetype: 'private_message'
|
||
}
|
||
end.to change { Topic.count }.by(1)
|
||
|
||
expect(response.status).to eq(200)
|
||
|
||
result = response.parsed_body
|
||
|
||
expect(result['success']).to eq(true)
|
||
expect(result['url']).to eq(Topic.last.relative_url)
|
||
end
|
||
end
|
||
end
|
||
|
||
context 'failure' do
|
||
it "returns JSON with a false success" do
|
||
sign_in(moderator)
|
||
post "/t/#{message.id}/move-posts.json", params: {
|
||
post_ids: [p2.id],
|
||
archetype: 'private_message'
|
||
}
|
||
expect(response.status).to eq(200)
|
||
result = response.parsed_body
|
||
expect(result['success']).to eq(false)
|
||
expect(result['url']).to be_blank
|
||
end
|
||
end
|
||
end
|
||
|
||
describe 'moving to an existing message' do
|
||
let!(:user) { sign_in(admin) }
|
||
fab!(:evil_trout) { Fabricate(:evil_trout) }
|
||
let(:message) { Fabricate(:private_message_topic) }
|
||
let(:p2) { Fabricate(:post, user: evil_trout, post_number: 2, topic: message) }
|
||
|
||
let(:dest_message) do
|
||
Fabricate(:private_message_topic, user: trust_level_4, topic_allowed_users: [
|
||
Fabricate.build(:topic_allowed_user, user: evil_trout)
|
||
])
|
||
end
|
||
|
||
context 'success' do
|
||
it "returns success" do
|
||
user
|
||
post "/t/#{message.id}/move-posts.json", params: {
|
||
post_ids: [p2.id],
|
||
destination_topic_id: dest_message.id,
|
||
archetype: 'private_message'
|
||
}
|
||
|
||
expect(response.status).to eq(200)
|
||
result = response.parsed_body
|
||
expect(result['success']).to eq(true)
|
||
expect(result['url']).to be_present
|
||
end
|
||
end
|
||
|
||
context 'failure' do
|
||
it "returns JSON with a false success" do
|
||
post "/t/#{message.id}/move-posts.json", params: {
|
||
post_ids: [p2.id],
|
||
archetype: 'private_message'
|
||
}
|
||
|
||
expect(response.status).to eq(200)
|
||
result = response.parsed_body
|
||
expect(result['success']).to eq(false)
|
||
expect(result['url']).to be_blank
|
||
end
|
||
end
|
||
end
|
||
end
|
||
|
||
describe '#merge_topic' do
|
||
it 'needs you to be logged in' do
|
||
post "/t/111/merge-topic.json", params: {
|
||
destination_topic_id: 345
|
||
}
|
||
expect(response.status).to eq(403)
|
||
end
|
||
|
||
describe 'merging into another topic' do
|
||
let(:p1) { Fabricate(:post, user: user) }
|
||
let(:topic) { p1.topic }
|
||
|
||
it "raises an error without destination_topic_id" do
|
||
sign_in(moderator)
|
||
post "/t/#{topic.id}/merge-topic.json"
|
||
expect(response.status).to eq(400)
|
||
end
|
||
|
||
it "raises an error when the user doesn't have permission to merge" do
|
||
sign_in(user)
|
||
post "/t/111/merge-topic.json", params: { destination_topic_id: 345 }
|
||
expect(response).to be_forbidden
|
||
end
|
||
|
||
let(:dest_topic) { Fabricate(:topic) }
|
||
|
||
context 'moves all the posts to the destination topic' do
|
||
it "returns success" do
|
||
sign_in(moderator)
|
||
post "/t/#{topic.id}/merge-topic.json", params: {
|
||
destination_topic_id: dest_topic.id
|
||
}
|
||
|
||
expect(response.status).to eq(200)
|
||
result = response.parsed_body
|
||
expect(result['success']).to eq(true)
|
||
expect(result['url']).to be_present
|
||
end
|
||
end
|
||
end
|
||
|
||
describe "merging into another topic as a group moderator" do
|
||
fab!(:group_user) { Fabricate(:group_user) }
|
||
fab!(:category) { Fabricate(:category, reviewable_by_group: group_user.group) }
|
||
fab!(:topic) { Fabricate(:topic, category: category) }
|
||
fab!(:p1) { Fabricate(:post, post_number: 1, topic: topic) }
|
||
fab!(:p2) { Fabricate(:post, post_number: 2, topic: topic) }
|
||
fab!(:dest_topic) { Fabricate(:topic) }
|
||
let(:user) { group_user.user }
|
||
|
||
before do
|
||
sign_in(user)
|
||
SiteSetting.enable_category_group_moderation = true
|
||
end
|
||
|
||
it "moves the posts" do
|
||
post "/t/#{topic.id}/merge-topic.json", params: {
|
||
destination_topic_id: dest_topic.id
|
||
}
|
||
|
||
expect(response.status).to eq(200)
|
||
result = response.parsed_body
|
||
expect(result['success']).to eq(true)
|
||
expect(result['url']).to be_present
|
||
end
|
||
|
||
it "does not allow posts to be moved to a private category" do
|
||
staff_category = Fabricate(:category)
|
||
staff_category.set_permissions(staff: :full)
|
||
staff_category.save!
|
||
dest_topic.update!(category: staff_category)
|
||
|
||
post "/t/#{topic.id}/merge-topic.json", params: {
|
||
destination_topic_id: dest_topic.id
|
||
}
|
||
|
||
expect(response).to be_forbidden
|
||
end
|
||
|
||
it "does not allow posts outside of the category to be moved" do
|
||
topic.update!(category: nil)
|
||
|
||
post "/t/#{topic.id}/merge-topic.json", params: {
|
||
destination_topic_id: dest_topic.id
|
||
}
|
||
|
||
expect(response).to be_forbidden
|
||
end
|
||
end
|
||
|
||
describe 'merging into another message' do
|
||
let(:message) { Fabricate(:private_message_topic, user: user) }
|
||
let!(:p1) { Fabricate(:post, topic: message, user: trust_level_4) }
|
||
let!(:p2) { Fabricate(:post, topic: message, reply_to_post_number: p1.post_number, user: user) }
|
||
|
||
it "raises an error without destination_topic_id" do
|
||
sign_in(moderator)
|
||
post "/t/#{message.id}/merge-topic.json", params: {
|
||
archetype: 'private_message'
|
||
}
|
||
expect(response.status).to eq(400)
|
||
end
|
||
|
||
it "raises an error when the user doesn't have permission to merge" do
|
||
sign_in(trust_level_4)
|
||
post "/t/#{message.id}/merge-topic.json", params: {
|
||
destination_topic_id: 345,
|
||
archetype: 'private_message'
|
||
}
|
||
expect(response).to be_forbidden
|
||
end
|
||
|
||
let(:dest_message) do
|
||
Fabricate(:private_message_topic, user: trust_level_4, topic_allowed_users: [
|
||
Fabricate.build(:topic_allowed_user, user: moderator)
|
||
])
|
||
end
|
||
|
||
context 'moves all the posts to the destination message' do
|
||
it "returns success" do
|
||
sign_in(moderator)
|
||
post "/t/#{message.id}/merge-topic.json", params: {
|
||
destination_topic_id: dest_message.id,
|
||
archetype: 'private_message'
|
||
}
|
||
|
||
expect(response.status).to eq(200)
|
||
result = response.parsed_body
|
||
expect(result['success']).to eq(true)
|
||
expect(result['url']).to be_present
|
||
end
|
||
end
|
||
end
|
||
end
|
||
|
||
describe '#change_post_owners' do
|
||
it 'needs you to be logged in' do
|
||
post "/t/111/change-owner.json", params: {
|
||
username: 'user_a',
|
||
post_ids: [1, 2, 3]
|
||
}
|
||
expect(response).to be_forbidden
|
||
end
|
||
|
||
describe 'forbidden to moderators' do
|
||
before do
|
||
sign_in(moderator)
|
||
end
|
||
it 'correctly denies' do
|
||
post "/t/111/change-owner.json", params: {
|
||
topic_id: 111, username: 'user_a', post_ids: [1, 2, 3]
|
||
}
|
||
expect(response).to be_forbidden
|
||
end
|
||
end
|
||
|
||
describe 'forbidden to trust_level_4s' do
|
||
before do
|
||
sign_in(trust_level_4)
|
||
end
|
||
|
||
it 'correctly denies' do
|
||
post "/t/111/change-owner.json", params: {
|
||
topic_id: 111, username: 'user_a', post_ids: [1, 2, 3]
|
||
}
|
||
expect(response).to be_forbidden
|
||
end
|
||
end
|
||
|
||
describe 'changing ownership' do
|
||
let!(:editor) { sign_in(admin) }
|
||
let(:topic) { Fabricate(:topic) }
|
||
fab!(:user_a) { Fabricate(:user) }
|
||
let(:p1) { Fabricate(:post, topic: topic) }
|
||
let(:p2) { Fabricate(:post, topic: topic) }
|
||
|
||
it "raises an error with a parameter missing" do
|
||
[
|
||
{ post_ids: [1, 2, 3] },
|
||
{ username: 'user_a' }
|
||
].each do |params|
|
||
post "/t/111/change-owner.json", params: params
|
||
expect(response.status).to eq(400)
|
||
end
|
||
end
|
||
|
||
it "changes the topic and posts ownership" do
|
||
post "/t/#{topic.id}/change-owner.json", params: {
|
||
username: user_a.username_lower, post_ids: [p1.id]
|
||
}
|
||
topic.reload
|
||
p1.reload
|
||
expect(response.status).to eq(200)
|
||
expect(topic.user.username).to eq(user_a.username)
|
||
expect(p1.user.username).to eq(user_a.username)
|
||
end
|
||
|
||
it "changes multiple posts" do
|
||
post "/t/#{topic.id}/change-owner.json", params: {
|
||
username: user_a.username_lower, post_ids: [p1.id, p2.id]
|
||
}
|
||
|
||
expect(response.status).to eq(200)
|
||
|
||
p1.reload
|
||
p2.reload
|
||
|
||
expect(p1.user).to_not eq(nil)
|
||
expect(p1.reload.user).to eq(p2.reload.user)
|
||
end
|
||
|
||
it "works with deleted users" do
|
||
deleted_user = user
|
||
t2 = Fabricate(:topic, user: deleted_user)
|
||
p3 = Fabricate(:post, topic: t2, user: deleted_user)
|
||
|
||
UserDestroyer.new(editor).destroy(deleted_user, delete_posts: true, context: 'test', delete_as_spammer: true)
|
||
|
||
post "/t/#{t2.id}/change-owner.json", params: {
|
||
username: user_a.username_lower, post_ids: [p3.id]
|
||
}
|
||
|
||
expect(response.status).to eq(200)
|
||
t2.reload
|
||
p3.reload
|
||
expect(t2.deleted_at).to be_nil
|
||
expect(p3.user).to eq(user_a)
|
||
end
|
||
|
||
it "removes likes by new owner" do
|
||
now = Time.zone.now
|
||
freeze_time(now - 1.day)
|
||
PostActionCreator.like(user_a, p1)
|
||
p1.reload
|
||
freeze_time(now)
|
||
post "/t/#{topic.id}/change-owner.json", params: {
|
||
username: user_a.username_lower, post_ids: [p1.id]
|
||
}
|
||
topic.reload
|
||
p1.reload
|
||
expect(response.status).to eq(200)
|
||
expect(topic.user.username).to eq(user_a.username)
|
||
expect(p1.user.username).to eq(user_a.username)
|
||
expect(p1.like_count).to eq(0)
|
||
end
|
||
end
|
||
end
|
||
|
||
describe '#change_timestamps' do
|
||
let(:params) { { timestamp: Time.zone.now } }
|
||
|
||
it 'needs you to be logged in' do
|
||
put "/t/1/change-timestamp.json", params: params
|
||
expect(response.status).to eq(403)
|
||
end
|
||
|
||
describe "forbidden to trust_level_4" do
|
||
before do
|
||
sign_in(trust_level_4)
|
||
end
|
||
|
||
it 'correctly denies' do
|
||
put "/t/1/change-timestamp.json", params: params
|
||
expect(response).to be_forbidden
|
||
end
|
||
end
|
||
|
||
describe 'changing timestamps' do
|
||
before do
|
||
freeze_time
|
||
sign_in(moderator)
|
||
end
|
||
|
||
let(:old_timestamp) { Time.zone.now }
|
||
let(:new_timestamp) { old_timestamp - 1.day }
|
||
let!(:topic) { Fabricate(:topic, created_at: old_timestamp) }
|
||
let!(:p1) { Fabricate(:post, topic: topic, created_at: old_timestamp) }
|
||
let!(:p2) { Fabricate(:post, topic: topic, created_at: old_timestamp + 1.day) }
|
||
|
||
it 'should update the timestamps of selected posts' do
|
||
# try to see if we fail with invalid first
|
||
put "/t/1/change-timestamp.json"
|
||
expect(response.status).to eq(400)
|
||
|
||
put "/t/#{topic.id}/change-timestamp.json", params: {
|
||
timestamp: new_timestamp.to_f
|
||
}
|
||
|
||
expect(response.status).to eq(200)
|
||
expect(topic.reload.created_at).to eq_time(new_timestamp)
|
||
expect(p1.reload.created_at).to eq_time(new_timestamp)
|
||
expect(p2.reload.created_at).to eq_time(old_timestamp)
|
||
end
|
||
|
||
it 'should create a staff log entry' do
|
||
put "/t/#{topic.id}/change-timestamp.json", params: {
|
||
timestamp: new_timestamp.to_f
|
||
}
|
||
|
||
log = UserHistory.last
|
||
expect(log.acting_user_id).to eq(moderator.id)
|
||
expect(log.topic_id).to eq(topic.id)
|
||
expect(log.new_value).to eq(new_timestamp.utc.to_s)
|
||
expect(log.previous_value).to eq(old_timestamp.utc.to_s)
|
||
end
|
||
end
|
||
end
|
||
|
||
describe '#clear_pin' do
|
||
it 'needs you to be logged in' do
|
||
put "/t/1/clear-pin.json"
|
||
expect(response.status).to eq(403)
|
||
end
|
||
|
||
context 'when logged in' do
|
||
let(:topic) { Fabricate(:topic) }
|
||
let(:pm) { Fabricate(:private_message_topic) }
|
||
before do
|
||
sign_in(user)
|
||
end
|
||
|
||
it "fails when the user can't see the topic" do
|
||
put "/t/#{pm.id}/clear-pin.json"
|
||
expect(response).to be_forbidden
|
||
end
|
||
|
||
describe 'when the user can see the topic' do
|
||
it "succeeds" do
|
||
expect do
|
||
put "/t/#{topic.id}/clear-pin.json"
|
||
end.to change { TopicUser.where(topic_id: topic.id, user_id: user.id).count }.by(1)
|
||
expect(response.status).to eq(200)
|
||
end
|
||
end
|
||
end
|
||
end
|
||
|
||
describe '#status' do
|
||
it 'needs you to be logged in' do
|
||
put "/t/1/status.json", params: {
|
||
status: 'visible', enabled: true
|
||
}
|
||
expect(response.status).to eq(403)
|
||
end
|
||
|
||
describe 'when logged in as a moderator' do
|
||
let(:topic) { Fabricate(:topic) }
|
||
before do
|
||
sign_in(moderator)
|
||
end
|
||
|
||
it "raises an exception if you can't change it" do
|
||
sign_in(user)
|
||
put "/t/#{topic.id}/status.json", params: {
|
||
status: 'visible', enabled: 'true'
|
||
}
|
||
expect(response).to be_forbidden
|
||
end
|
||
|
||
it 'requires the status parameter' do
|
||
put "/t/#{topic.id}/status.json", params: { enabled: true }
|
||
expect(response.status).to eq(400)
|
||
end
|
||
|
||
it 'requires the enabled parameter' do
|
||
put "/t/#{topic.id}/status.json", params: { status: 'visible' }
|
||
expect(response.status).to eq(400)
|
||
end
|
||
|
||
it 'raises an error with a status not in the allowlist' do
|
||
put "/t/#{topic.id}/status.json", params: {
|
||
status: 'title', enabled: 'true'
|
||
}
|
||
expect(response.status).to eq(400)
|
||
end
|
||
|
||
it 'should update the status of the topic correctly' do
|
||
topic = Fabricate(:topic, user: user, closed: true)
|
||
Fabricate(:topic_timer, topic: topic, status_type: TopicTimer.types[:open])
|
||
|
||
put "/t/#{topic.id}/status.json", params: {
|
||
status: 'closed', enabled: 'false'
|
||
}
|
||
|
||
expect(response.status).to eq(200)
|
||
expect(topic.reload.closed).to eq(false)
|
||
expect(topic.topic_timers).to eq([])
|
||
|
||
body = response.parsed_body
|
||
|
||
expect(body['topic_status_update']).to eq(nil)
|
||
end
|
||
end
|
||
|
||
describe 'when logged in as a group member with reviewable status' do
|
||
fab!(:group_user) { Fabricate(:group_user) }
|
||
fab!(:category) { Fabricate(:category, reviewable_by_group: group_user.group) }
|
||
fab!(:topic) { Fabricate(:topic, category: category) }
|
||
let(:user) { group_user.user }
|
||
|
||
before do
|
||
sign_in(user)
|
||
SiteSetting.enable_category_group_moderation = true
|
||
end
|
||
|
||
it 'should allow a group moderator to close a topic' do
|
||
put "/t/#{topic.id}/status.json", params: {
|
||
status: 'closed', enabled: 'true'
|
||
}
|
||
|
||
expect(response.status).to eq(200)
|
||
expect(topic.reload.closed).to eq(true)
|
||
expect(topic.posts.last.action_code).to eq('closed.enabled')
|
||
end
|
||
|
||
it 'should allow a group moderator to open a closed topic' do
|
||
topic.update!(closed: true)
|
||
|
||
expect do
|
||
put "/t/#{topic.id}/status.json", params: {
|
||
status: 'closed', enabled: 'false'
|
||
}
|
||
end.to change { topic.reload.posts.count }.by(1)
|
||
|
||
expect(response.status).to eq(200)
|
||
expect(topic.reload.closed).to eq(false)
|
||
expect(topic.posts.last.action_code).to eq('closed.disabled')
|
||
end
|
||
|
||
it 'should allow a group moderator to archive a topic' do
|
||
expect do
|
||
put "/t/#{topic.id}/status.json", params: {
|
||
status: 'archived', enabled: 'true'
|
||
}
|
||
end.to change { topic.reload.posts.count }.by(1)
|
||
|
||
expect(response.status).to eq(200)
|
||
expect(topic.reload.archived).to eq(true)
|
||
expect(topic.posts.last.action_code).to eq('archived.enabled')
|
||
end
|
||
|
||
it 'should allow a group moderator to unarchive an archived topic' do
|
||
topic.update!(archived: true)
|
||
|
||
put "/t/#{topic.id}/status.json", params: {
|
||
status: 'archived', enabled: 'false'
|
||
}
|
||
|
||
expect(response.status).to eq(200)
|
||
expect(topic.reload.archived).to eq(false)
|
||
expect(topic.posts.last.action_code).to eq('archived.disabled')
|
||
end
|
||
|
||
it 'should not allow a group moderator to pin a topic' do
|
||
put "/t/#{topic.id}/status.json", params: {
|
||
status: 'pinned', enabled: 'true'
|
||
}
|
||
|
||
expect(response.status).to eq(403)
|
||
expect(topic.reload.pinned_at).to eq(nil)
|
||
end
|
||
|
||
it 'should allow a group moderator to unlist a topic' do
|
||
put "/t/#{topic.id}/status.json", params: {
|
||
status: 'visible', enabled: 'false'
|
||
}
|
||
|
||
expect(response.status).to eq(200)
|
||
expect(topic.reload.visible).to eq(false)
|
||
expect(topic.posts.last.action_code).to eq('visible.disabled')
|
||
end
|
||
|
||
it 'should allow a group moderator to list an unlisted topic' do
|
||
topic.update!(visible: false)
|
||
|
||
put "/t/#{topic.id}/status.json", params: {
|
||
status: 'visible', enabled: 'true'
|
||
}
|
||
|
||
expect(response.status).to eq(200)
|
||
expect(topic.reload.visible).to eq(true)
|
||
expect(topic.posts.last.action_code).to eq('visible.enabled')
|
||
end
|
||
end
|
||
end
|
||
|
||
describe '#destroy_timings' do
|
||
it 'needs you to be logged in' do
|
||
delete "/t/1/timings.json"
|
||
expect(response.status).to eq(403)
|
||
end
|
||
|
||
def topic_user_post_timings_count(user, topic)
|
||
[TopicUser, PostTiming].map do |klass|
|
||
klass.where(user: user, topic: topic).count
|
||
end
|
||
end
|
||
|
||
context 'for last post only' do
|
||
it 'should allow you to retain topic timing but remove last post only' do
|
||
freeze_time
|
||
|
||
post1 = create_post
|
||
user = post1.user
|
||
|
||
topic = post1.topic
|
||
|
||
post2 = create_post(topic_id: topic.id)
|
||
|
||
PostTiming.create!(topic: topic, user: user, post_number: 2, msecs: 100)
|
||
|
||
user.user_stat.update!(first_unread_at: Time.now + 1.week)
|
||
|
||
topic_user = TopicUser.find_by(
|
||
topic_id: topic.id,
|
||
user_id: user.id,
|
||
)
|
||
|
||
topic_user.update!(
|
||
last_read_post_number: 2,
|
||
highest_seen_post_number: 2
|
||
)
|
||
|
||
# ensure we have 2 notifications
|
||
# fake notification on topic but it is read
|
||
first_notification = Notification.create!(
|
||
user_id: user.id,
|
||
topic_id: topic.id,
|
||
data: "{}",
|
||
read: true,
|
||
notification_type: 1
|
||
)
|
||
|
||
freeze_time 1.minute.from_now
|
||
PostAlerter.post_created(post2)
|
||
|
||
second_notification = user.notifications.where(topic_id: topic.id).order(created_at: :desc).first
|
||
second_notification.update!(read: true)
|
||
|
||
sign_in(user)
|
||
|
||
delete "/t/#{topic.id}/timings.json?last=1"
|
||
|
||
expect(PostTiming.where(topic: topic, user: user, post_number: 2).exists?).to eq(false)
|
||
expect(PostTiming.where(topic: topic, user: user, post_number: 1).exists?).to eq(true)
|
||
|
||
expect(TopicUser.where(topic: topic, user: user, last_read_post_number: 1, highest_seen_post_number: 1).exists?).to eq(true)
|
||
|
||
user.user_stat.reload
|
||
expect(user.user_stat.first_unread_at).to eq_time(topic.updated_at)
|
||
|
||
first_notification.reload
|
||
second_notification.reload
|
||
expect(first_notification.read).to eq(true)
|
||
expect(second_notification.read).to eq(false)
|
||
|
||
PostDestroyer.new(admin, post2).destroy
|
||
|
||
delete "/t/#{topic.id}/timings.json?last=1"
|
||
|
||
expect(PostTiming.where(topic: topic, user: user, post_number: 1).exists?).to eq(false)
|
||
expect(TopicUser.where(topic: topic, user: user, last_read_post_number: nil, highest_seen_post_number: nil).exists?).to eq(true)
|
||
end
|
||
end
|
||
|
||
context 'when logged in' do
|
||
before do
|
||
@user = sign_in(user)
|
||
@topic = Fabricate(:topic, user: @user)
|
||
Fabricate(:post, user: @user, topic: @topic, post_number: 2)
|
||
TopicUser.create!(topic: @topic, user: @user)
|
||
PostTiming.create!(topic: @topic, user: @user, post_number: 2, msecs: 1000)
|
||
end
|
||
|
||
it 'deletes the forum topic user and post timings records' do
|
||
expect do
|
||
delete "/t/#{@topic.id}/timings.json"
|
||
end.to change { topic_user_post_timings_count(@user, @topic) }.from([1, 1]).to([0, 0])
|
||
end
|
||
end
|
||
end
|
||
|
||
describe '#mute/unmute' do
|
||
it 'needs you to be logged in' do
|
||
put "/t/99/mute.json"
|
||
expect(response.status).to eq(403)
|
||
end
|
||
|
||
it 'needs you to be logged in' do
|
||
put "/t/99/unmute.json"
|
||
expect(response.status).to eq(403)
|
||
end
|
||
end
|
||
|
||
describe '#recover' do
|
||
it "won't allow us to recover a topic when we're not logged in" do
|
||
put "/t/1/recover.json"
|
||
expect(response.status).to eq(403)
|
||
end
|
||
|
||
describe 'when logged in' do
|
||
let(:topic) { Fabricate(:topic, user: user, deleted_at: Time.now, deleted_by: moderator) }
|
||
let!(:post) { Fabricate(:post, user: user, topic: topic, post_number: 1, deleted_at: Time.now, deleted_by: moderator) }
|
||
|
||
describe 'without access' do
|
||
it "raises an exception when the user doesn't have permission to delete the topic" do
|
||
sign_in(user)
|
||
put "/t/#{topic.id}/recover.json"
|
||
expect(response).to be_forbidden
|
||
end
|
||
end
|
||
|
||
context 'with permission' do
|
||
before do
|
||
sign_in(moderator)
|
||
end
|
||
|
||
it 'succeeds' do
|
||
put "/t/#{topic.id}/recover.json"
|
||
topic.reload
|
||
post.reload
|
||
expect(response.status).to eq(200)
|
||
expect(topic.trashed?).to be_falsey
|
||
expect(post.trashed?).to be_falsey
|
||
end
|
||
end
|
||
end
|
||
end
|
||
|
||
describe '#delete' do
|
||
it "won't allow us to delete a topic when we're not logged in" do
|
||
delete "/t/1.json"
|
||
expect(response.status).to eq(403)
|
||
end
|
||
|
||
describe 'when logged in' do
|
||
let(:topic) { Fabricate(:topic, user: user, created_at: 48.hours.ago) }
|
||
let!(:post) { Fabricate(:post, topic: topic, user: user, post_number: 1) }
|
||
|
||
describe 'without access' do
|
||
it "raises an exception when the user doesn't have permission to delete the topic" do
|
||
sign_in(user)
|
||
delete "/t/#{topic.id}.json"
|
||
expect(response.status).to eq(422)
|
||
end
|
||
end
|
||
|
||
describe 'with permission' do
|
||
before do
|
||
sign_in(moderator)
|
||
end
|
||
|
||
it 'succeeds' do
|
||
delete "/t/#{topic.id}.json"
|
||
expect(response.status).to eq(200)
|
||
topic.reload
|
||
expect(topic.trashed?).to be_truthy
|
||
end
|
||
end
|
||
end
|
||
end
|
||
|
||
describe '#id_for_slug' do
|
||
let(:topic) { Fabricate(:post).topic }
|
||
let(:pm) { Fabricate(:private_message_topic) }
|
||
|
||
it "returns JSON for the slug" do
|
||
get "/t/id_for/#{topic.slug}.json"
|
||
expect(response.status).to eq(200)
|
||
json = response.parsed_body
|
||
expect(json['topic_id']).to eq(topic.id)
|
||
expect(json['url']).to eq(topic.url)
|
||
expect(json['slug']).to eq(topic.slug)
|
||
end
|
||
|
||
it "returns invalid access if the user can't see the topic" do
|
||
get "/t/id_for/#{pm.slug}.json"
|
||
expect(response).to be_forbidden
|
||
end
|
||
end
|
||
|
||
describe '#update' do
|
||
it "won't allow us to update a topic when we're not logged in" do
|
||
put "/t/1.json", params: { slug: 'xyz' }
|
||
expect(response.status).to eq(403)
|
||
end
|
||
|
||
describe 'when logged in' do
|
||
let(:topic) { Fabricate(:topic, user: user) }
|
||
|
||
before do
|
||
Fabricate(:post, topic: topic)
|
||
SiteSetting.editing_grace_period = 0
|
||
sign_in(user)
|
||
end
|
||
|
||
it 'can not change category to a disallowed category' do
|
||
category = Fabricate(:category)
|
||
category.set_permissions(staff: :full)
|
||
category.save!
|
||
|
||
put "/t/#{topic.id}.json", params: { category_id: category.id }
|
||
|
||
expect(response.status).to eq(403)
|
||
expect(topic.reload.category_id).not_to eq(category.id)
|
||
end
|
||
|
||
it 'can not move to a category that requires topic approval' do
|
||
category = Fabricate(:category)
|
||
category.custom_fields[Category::REQUIRE_TOPIC_APPROVAL] = true
|
||
category.save!
|
||
|
||
put "/t/#{topic.id}.json", params: { category_id: category.id }
|
||
|
||
expect(response.status).to eq(403)
|
||
expect(topic.reload.category_id).not_to eq(category.id)
|
||
end
|
||
|
||
context 'updating shared drafts' do
|
||
fab!(:shared_drafts_category) { Fabricate(:category) }
|
||
fab!(:topic) { Fabricate(:topic, category: shared_drafts_category) }
|
||
fab!(:shared_draft) { Fabricate(:shared_draft, topic: topic, category: Fabricate(:category)) }
|
||
|
||
it 'changes destination category' do
|
||
category = Fabricate(:category)
|
||
|
||
put "/t/#{topic.id}.json", params: { category_id: category.id }
|
||
|
||
expect(response.status).to eq(403)
|
||
expect(topic.shared_draft.category_id).not_to eq(category.id)
|
||
end
|
||
end
|
||
|
||
describe 'without permission' do
|
||
it "raises an exception when the user doesn't have permission to update the topic" do
|
||
topic.update!(archived: true)
|
||
put "/t/#{topic.slug}/#{topic.id}.json"
|
||
|
||
expect(response.status).to eq(403)
|
||
end
|
||
end
|
||
|
||
describe 'with permission' do
|
||
fab!(:post_hook) { Fabricate(:post_web_hook) }
|
||
fab!(:topic_hook) { Fabricate(:topic_web_hook) }
|
||
|
||
it 'succeeds' do
|
||
put "/t/#{topic.slug}/#{topic.id}.json"
|
||
|
||
expect(response.status).to eq(200)
|
||
expect(response.parsed_body['basic_topic']).to be_present
|
||
end
|
||
|
||
it "throws an error if it could not be saved" do
|
||
PostRevisor.any_instance.stubs(:should_revise?).returns(false)
|
||
put "/t/#{topic.slug}/#{topic.id}.json", params: { title: "brand new title" }
|
||
|
||
expect(response.status).to eq(422)
|
||
expect(response.parsed_body['errors'].first).to eq(
|
||
I18n.t("activerecord.errors.models.topic.attributes.base.unable_to_update")
|
||
)
|
||
end
|
||
|
||
it "can update a topic to an uncategorized topic" do
|
||
topic.update!(category: Fabricate(:category))
|
||
|
||
put "/t/#{topic.slug}/#{topic.id}.json", params: {
|
||
category_id: ""
|
||
}
|
||
|
||
expect(response.status).to eq(200)
|
||
expect(topic.reload.category_id).to eq(SiteSetting.uncategorized_category_id)
|
||
end
|
||
|
||
it 'allows a change of title' do
|
||
put "/t/#{topic.slug}/#{topic.id}.json", params: {
|
||
title: 'This is a new title for the topic'
|
||
}
|
||
|
||
topic.reload
|
||
expect(topic.title).to eq('This is a new title for the topic')
|
||
|
||
expect(Jobs::EmitWebHookEvent.jobs.length).to eq(2)
|
||
job_args = Jobs::EmitWebHookEvent.jobs[0]["args"].first
|
||
|
||
expect(job_args["event_name"]).to eq("post_edited")
|
||
payload = JSON.parse(job_args["payload"])
|
||
expect(payload["topic_title"]).to eq('This is a new title for the topic')
|
||
end
|
||
|
||
it "returns errors with invalid titles" do
|
||
put "/t/#{topic.slug}/#{topic.id}.json", params: {
|
||
title: 'asdf'
|
||
}
|
||
|
||
expect(response.status).to eq(422)
|
||
expect(response.parsed_body['errors']).to match_array([/Title is too short/, /Title seems unclear/])
|
||
end
|
||
|
||
it "returns errors when the rate limit is exceeded" do
|
||
EditRateLimiter.any_instance.expects(:performed!).raises(RateLimiter::LimitExceeded.new(60))
|
||
|
||
put "/t/#{topic.slug}/#{topic.id}.json", params: {
|
||
title: 'This is a new title for the topic'
|
||
}
|
||
|
||
expect(response.status).to eq(429)
|
||
end
|
||
|
||
it "returns errors with invalid categories" do
|
||
put "/t/#{topic.slug}/#{topic.id}.json", params: {
|
||
category_id: -1
|
||
}
|
||
|
||
expect(response.status).to eq(422)
|
||
end
|
||
|
||
it "doesn't call the PostRevisor when there is no changes" do
|
||
expect do
|
||
put "/t/#{topic.slug}/#{topic.id}.json", params: {
|
||
category_id: topic.category_id
|
||
}
|
||
end.not_to change(PostRevision.all, :count)
|
||
|
||
expect(response.status).to eq(200)
|
||
end
|
||
|
||
describe "when first post is locked" do
|
||
it "blocks non-staff from editing even if 'trusted_users_can_edit_others' is true" do
|
||
SiteSetting.trusted_users_can_edit_others = true
|
||
user.update!(trust_level: 3)
|
||
topic.first_post.update!(locked_by_id: admin.id)
|
||
|
||
put "/t/#{topic.slug}/#{topic.id}.json", params: {
|
||
title: topic.title + " hello"
|
||
}
|
||
|
||
expect(response.status).to eq(403)
|
||
end
|
||
|
||
it "allows staff to edit" do
|
||
sign_in(Fabricate(:admin))
|
||
topic.first_post.update!(locked_by_id: admin.id)
|
||
|
||
put "/t/#{topic.slug}/#{topic.id}.json", params: {
|
||
title: topic.title + " hello"
|
||
}
|
||
expect(response.status).to eq(200)
|
||
end
|
||
end
|
||
|
||
context 'tags' do
|
||
fab!(:tag) { Fabricate(:tag) }
|
||
|
||
before do
|
||
SiteSetting.tagging_enabled = true
|
||
end
|
||
|
||
it "can add a tag to topic" do
|
||
expect do
|
||
put "/t/#{topic.slug}/#{topic.id}.json", params: {
|
||
tags: [tag.name]
|
||
}
|
||
end.to change { topic.reload.first_post.revisions.count }.by(1)
|
||
|
||
expect(response.status).to eq(200)
|
||
expect(topic.tags.pluck(:id)).to contain_exactly(tag.id)
|
||
end
|
||
|
||
it "can create a tag" do
|
||
SiteSetting.min_trust_to_create_tag = 0
|
||
expect do
|
||
put "/t/#{topic.slug}/#{topic.id}.json", params: {
|
||
tags: ["newtag"]
|
||
}
|
||
end.to change { topic.reload.first_post.revisions.count }.by(1)
|
||
|
||
expect(response.status).to eq(200)
|
||
expect(topic.reload.tags.pluck(:name)).to contain_exactly("newtag")
|
||
end
|
||
|
||
it "can change the category and create a new tag" do
|
||
SiteSetting.min_trust_to_create_tag = 0
|
||
category = Fabricate(:category)
|
||
expect do
|
||
put "/t/#{topic.slug}/#{topic.id}.json", params: {
|
||
tags: ["newtag"],
|
||
category_id: category.id
|
||
}
|
||
end.to change { topic.reload.first_post.revisions.count }.by(1)
|
||
|
||
expect(response.status).to eq(200)
|
||
expect(topic.reload.tags.pluck(:name)).to contain_exactly("newtag")
|
||
end
|
||
|
||
it "can add a tag to wiki topic" do
|
||
SiteSetting.min_trust_to_edit_wiki_post = 2
|
||
topic.first_post.update!(wiki: true)
|
||
user = Fabricate(:user)
|
||
sign_in(user)
|
||
|
||
expect do
|
||
put "/t/#{topic.id}/tags.json", params: {
|
||
tags: [tag.name]
|
||
}
|
||
end.not_to change { topic.reload.first_post.revisions.count }
|
||
|
||
expect(response.status).to eq(403)
|
||
user.update!(trust_level: 2)
|
||
|
||
expect do
|
||
put "/t/#{topic.id}/tags.json", params: {
|
||
tags: [tag.name]
|
||
}
|
||
end.to change { topic.reload.first_post.revisions.count }.by(1)
|
||
|
||
expect(response.status).to eq(200)
|
||
expect(topic.tags.pluck(:id)).to contain_exactly(tag.id)
|
||
end
|
||
|
||
it 'does not remove tag if no params is given' do
|
||
topic.tags << tag
|
||
|
||
expect do
|
||
put "/t/#{topic.slug}/#{topic.id}.json"
|
||
end.to_not change { topic.reload.tags.count }
|
||
|
||
expect(response.status).to eq(200)
|
||
end
|
||
|
||
it 'can remove a tag' do
|
||
topic.tags << tag
|
||
|
||
expect do
|
||
put "/t/#{topic.slug}/#{topic.id}.json", params: {
|
||
tags: [""]
|
||
}
|
||
end.to change { topic.reload.first_post.revisions.count }.by(1)
|
||
|
||
expect(response.status).to eq(200)
|
||
expect(topic.tags).to eq([])
|
||
end
|
||
end
|
||
|
||
context 'when topic is private' do
|
||
before do
|
||
topic.update!(
|
||
archetype: Archetype.private_message,
|
||
category: nil,
|
||
allowed_users: [topic.user]
|
||
)
|
||
end
|
||
|
||
context 'when there are no changes' do
|
||
it 'does not call the PostRevisor' do
|
||
expect do
|
||
put "/t/#{topic.slug}/#{topic.id}.json", params: {
|
||
category_id: topic.category_id
|
||
}
|
||
end.not_to change(PostRevision.all, :count)
|
||
|
||
expect(response.status).to eq(200)
|
||
end
|
||
end
|
||
end
|
||
|
||
context 'updating to a category with restricted tags' do
|
||
fab!(:category) { Fabricate(:category) }
|
||
fab!(:restricted_category) { Fabricate(:category) }
|
||
fab!(:tag1) { Fabricate(:tag) }
|
||
fab!(:tag2) { Fabricate(:tag) }
|
||
let(:tag3) { Fabricate(:tag) }
|
||
let!(:tag_group_1) { Fabricate(:tag_group, tag_names: [tag1.name]) }
|
||
fab!(:tag_group_2) { Fabricate(:tag_group) }
|
||
|
||
before do
|
||
SiteSetting.tagging_enabled = true
|
||
topic.update!(tags: [tag1])
|
||
end
|
||
|
||
it 'can’t change to a category disallowing this topic current tags' do
|
||
restricted_category.allowed_tags = [tag2.name]
|
||
|
||
put "/t/#{topic.slug}/#{topic.id}.json", params: { category_id: restricted_category.id }
|
||
|
||
result = response.parsed_body
|
||
|
||
expect(response.status).to eq(422)
|
||
expect(result['errors']).to be_present
|
||
expect(topic.reload.category_id).not_to eq(restricted_category.id)
|
||
end
|
||
|
||
it 'can’t change to a category disallowing this topic current tag (through tag_group)' do
|
||
tag_group_2.tags = [tag2]
|
||
restricted_category.allowed_tag_groups = [tag_group_2.name]
|
||
|
||
put "/t/#{topic.slug}/#{topic.id}.json", params: { category_id: restricted_category.id }
|
||
|
||
result = response.parsed_body
|
||
|
||
expect(response.status).to eq(422)
|
||
expect(result['errors']).to be_present
|
||
expect(topic.reload.category_id).not_to eq(restricted_category.id)
|
||
end
|
||
|
||
it 'can change to a category allowing this topic current tags' do
|
||
restricted_category.allowed_tags = [tag1.name]
|
||
|
||
put "/t/#{topic.slug}/#{topic.id}.json", params: { category_id: restricted_category.id }
|
||
|
||
expect(response.status).to eq(200)
|
||
end
|
||
|
||
it 'can change to a category allowing this topic current tags (through tag_group)' do
|
||
tag_group_1.tags = [tag1]
|
||
restricted_category.allowed_tag_groups = [tag_group_1.name]
|
||
|
||
put "/t/#{topic.slug}/#{topic.id}.json", params: { category_id: restricted_category.id }
|
||
|
||
expect(response.status).to eq(200)
|
||
end
|
||
|
||
it 'can change to a category allowing any tag' do
|
||
put "/t/#{topic.slug}/#{topic.id}.json", params: { category_id: category.id }
|
||
|
||
expect(response.status).to eq(200)
|
||
end
|
||
|
||
it 'can’t add a category-only tags from another category to a category' do
|
||
restricted_category.allowed_tags = [tag2.name]
|
||
|
||
put "/t/#{topic.slug}/#{topic.id}.json", params: {
|
||
tags: [tag2.name],
|
||
category_id: category.id
|
||
}
|
||
|
||
result = response.parsed_body
|
||
expect(response.status).to eq(422)
|
||
expect(result['errors']).to be_present
|
||
expect(result['errors'][0]).to include(tag2.name)
|
||
expect(topic.reload.category_id).not_to eq(restricted_category.id)
|
||
end
|
||
|
||
it 'allows category change when topic has a hidden tag' do
|
||
Fabricate(:tag_group, permissions: { "staff" => 1 }, tag_names: [tag1.name])
|
||
|
||
put "/t/#{topic.slug}/#{topic.id}.json", params: {
|
||
category_id: category.id
|
||
}
|
||
|
||
result = response.parsed_body
|
||
expect(response.status).to eq(200)
|
||
expect(topic.reload.tags).to include(tag1)
|
||
end
|
||
|
||
it 'allows category change when topic has a read-only tag' do
|
||
Fabricate(:tag_group, permissions: { "staff" => 1, "everyone" => 3 }, tag_names: [tag3.name])
|
||
topic.update!(tags: [tag3])
|
||
|
||
put "/t/#{topic.slug}/#{topic.id}.json", params: {
|
||
category_id: category.id
|
||
}
|
||
|
||
result = response.parsed_body
|
||
expect(response.status).to eq(200)
|
||
expect(topic.reload.tags).to contain_exactly(tag3)
|
||
end
|
||
|
||
it 'does not leak tag name when trying to use a staff tag' do
|
||
Fabricate(:tag_group, permissions: { "staff" => 1 }, tag_names: [tag3.name])
|
||
|
||
put "/t/#{topic.slug}/#{topic.id}.json", params: {
|
||
tags: [tag3.name],
|
||
category_id: category.id
|
||
}
|
||
|
||
result = response.parsed_body
|
||
expect(response.status).to eq(422)
|
||
expect(result['errors']).to be_present
|
||
expect(result['errors'][0]).not_to include(tag3.name)
|
||
end
|
||
|
||
it 'will clean tag params' do
|
||
restricted_category.allowed_tags = [tag2.name]
|
||
|
||
put "/t/#{topic.slug}/#{topic.id}.json", params: {
|
||
tags: [""],
|
||
category_id: restricted_category.id
|
||
}
|
||
|
||
result = response.parsed_body
|
||
expect(response.status).to eq(200)
|
||
end
|
||
end
|
||
|
||
context "allow_uncategorized_topics is false" do
|
||
before do
|
||
SiteSetting.allow_uncategorized_topics = false
|
||
end
|
||
|
||
it "can add a category to an uncategorized topic" do
|
||
category = Fabricate(:category)
|
||
|
||
put "/t/#{topic.slug}/#{topic.id}.json", params: {
|
||
category_id: category.id
|
||
}
|
||
|
||
expect(response.status).to eq(200)
|
||
expect(topic.reload.category).to eq(category)
|
||
end
|
||
end
|
||
end
|
||
end
|
||
end
|
||
|
||
describe '#show' do
|
||
let(:private_topic) { Fabricate(:private_message_topic) }
|
||
let(:topic) { Fabricate(:post).topic }
|
||
|
||
let!(:p1) { Fabricate(:post, user: topic.user) }
|
||
let!(:p2) { Fabricate(:post, user: topic.user) }
|
||
|
||
describe 'when topic is not allowed' do
|
||
it 'should return the right response' do
|
||
SiteSetting.detailed_404 = true
|
||
sign_in(user)
|
||
|
||
get "/t/#{private_topic.id}.json"
|
||
|
||
expect(response.status).to eq(403)
|
||
expect(response.body).to include(I18n.t('invalid_access'))
|
||
end
|
||
end
|
||
|
||
describe 'when topic is allowed to a group' do
|
||
let(:group) { Fabricate(:group, public_admission: true) }
|
||
let(:category) do
|
||
Fabricate(:category_with_definition).tap do |category|
|
||
category.set_permissions(group => :full)
|
||
category.save!
|
||
end
|
||
end
|
||
let(:topic) { Fabricate(:topic, category: category) }
|
||
|
||
before do
|
||
SiteSetting.detailed_404 = true
|
||
end
|
||
|
||
it 'shows a descriptive error message containing the group name' do
|
||
get "/t/#{topic.id}.json"
|
||
|
||
html = CGI.unescapeHTML(response.parsed_body["extras"]["html"])
|
||
expect(response.status).to eq(403)
|
||
expect(html).to include(I18n.t('not_in_group.title_topic', group: group.name))
|
||
expect(html).to include(I18n.t('not_in_group.join_group'))
|
||
end
|
||
end
|
||
|
||
it 'correctly renders canoicals' do
|
||
get "/t/#{topic.id}", params: { slug: topic.slug }
|
||
|
||
expect(response.status).to eq(200)
|
||
expect(css_select("link[rel=canonical]").length).to eq(1)
|
||
expect(response.headers["Cache-Control"]).to eq("no-cache, no-store")
|
||
end
|
||
|
||
it 'returns 301 even if slug does not match URL' do
|
||
# in the past we had special logic for unlisted topics
|
||
# we would require slug unless you made a json call
|
||
# this was not really providing any security
|
||
#
|
||
# we no longer require a topic be visible to perform url correction
|
||
# if you need to properly hide a topic for users use a secure category
|
||
# or a PM
|
||
topic = Fabricate(:topic, visible: false)
|
||
Fabricate(:post, topic: topic)
|
||
|
||
get "/t/#{topic.id}.json", params: { slug: topic.slug }
|
||
expect(response.status).to eq(200)
|
||
|
||
get "/t/#{topic.id}.json", params: { slug: "just-guessing" }
|
||
expect(response.status).to eq(301)
|
||
|
||
get "/t/#{topic.slug}.json"
|
||
expect(response.status).to eq(301)
|
||
end
|
||
|
||
it 'shows a topic correctly' do
|
||
get "/t/#{topic.slug}/#{topic.id}.json"
|
||
expect(response.status).to eq(200)
|
||
end
|
||
|
||
it 'return 404 for an invalid page' do
|
||
get "/t/#{topic.slug}/#{topic.id}.json", params: { page: 2 }
|
||
expect(response.status).to eq(404)
|
||
end
|
||
|
||
it 'can find a topic given a slug in the id param' do
|
||
get "/t/#{topic.slug}"
|
||
expect(response).to redirect_to(topic.relative_url)
|
||
end
|
||
|
||
it 'can find a topic when a slug has a number in front' do
|
||
another_topic = Fabricate(:post).topic
|
||
|
||
topic.update_column(:slug, "#{another_topic.id}-reasons-discourse-is-awesome")
|
||
get "/t/#{another_topic.id}-reasons-discourse-is-awesome"
|
||
|
||
expect(response).to redirect_to(topic.relative_url)
|
||
end
|
||
|
||
it 'keeps the post_number parameter around when redirecting' do
|
||
get "/t/#{topic.slug}", params: { post_number: 42 }
|
||
expect(response).to redirect_to(topic.relative_url + "/42")
|
||
end
|
||
|
||
it 'keeps the page around when redirecting' do
|
||
get "/t/#{topic.slug}", params: {
|
||
post_number: 42, page: 123
|
||
}
|
||
|
||
expect(response).to redirect_to(topic.relative_url + "/42?page=123")
|
||
end
|
||
|
||
it 'does not accept page params as an array' do
|
||
get "/t/#{topic.slug}", params: {
|
||
post_number: 42, page: [2]
|
||
}
|
||
|
||
expect(response).to redirect_to("#{topic.relative_url}/42?page=1")
|
||
end
|
||
|
||
it 'returns 404 when an invalid slug is given and no id' do
|
||
get "/t/nope-nope.json"
|
||
|
||
expect(response.status).to eq(404)
|
||
end
|
||
|
||
it 'returns a 404 when slug and topic id do not match a topic' do
|
||
get "/t/made-up-topic-slug/123456.json"
|
||
expect(response.status).to eq(404)
|
||
end
|
||
|
||
it 'returns a 404 for an ID that is larger than postgres limits' do
|
||
get "/t/made-up-topic-slug/5014217323220164041.json"
|
||
|
||
expect(response.status).to eq(404)
|
||
end
|
||
|
||
context 'a topic with nil slug exists' do
|
||
before do
|
||
nil_slug_topic = Fabricate(:topic)
|
||
Topic.connection.execute("update topics set slug=null where id = #{nil_slug_topic.id}") # can't find a way to set slug column to null using the model
|
||
end
|
||
|
||
it 'returns a 404 when slug and topic id do not match a topic' do
|
||
get "/t/made-up-topic-slug/123123.json"
|
||
expect(response.status).to eq(404)
|
||
end
|
||
end
|
||
|
||
context 'permission errors' do
|
||
fab!(:allowed_user) { Fabricate(:user) }
|
||
let(:allowed_group) { Fabricate(:group) }
|
||
let(:accessible_group) { Fabricate(:group, public_admission: true) }
|
||
let(:secure_category) do
|
||
c = Fabricate(:category)
|
||
c.permissions = [[allowed_group, :full]]
|
||
c.save
|
||
allowed_user.groups = [allowed_group]
|
||
allowed_user.save
|
||
c
|
||
end
|
||
let(:accessible_category) do
|
||
Fabricate(:category).tap do |c|
|
||
c.set_permissions(accessible_group => :full)
|
||
c.save!
|
||
end
|
||
end
|
||
let(:normal_topic) { Fabricate(:topic) }
|
||
let(:secure_topic) { Fabricate(:topic, category: secure_category) }
|
||
let(:private_topic) { Fabricate(:private_message_topic, user: allowed_user) }
|
||
let(:deleted_topic) { Fabricate(:deleted_topic) }
|
||
let(:deleted_secure_topic) { Fabricate(:topic, category: secure_category, deleted_at: 1.day.ago) }
|
||
let(:deleted_private_topic) { Fabricate(:private_message_topic, user: allowed_user, deleted_at: 1.day.ago) }
|
||
let(:nonexist_topic_id) { Topic.last.id + 10000 }
|
||
let(:secure_accessible_topic) { Fabricate(:topic, category: accessible_category) }
|
||
|
||
shared_examples "various scenarios" do |expected|
|
||
expected.each do |key, value|
|
||
it "returns #{value} for #{key}" do
|
||
slug = key == :nonexist ? "garbage-slug" : eval(key.to_s).slug
|
||
topic_id = key == :nonexist ? nonexist_topic_id : eval(key.to_s).id
|
||
get "/t/#{slug}/#{topic_id}.json"
|
||
expect(response.status).to eq(value)
|
||
end
|
||
end
|
||
|
||
expected_slug_response = expected[:secure_topic] == 200 ? 301 : expected[:secure_topic]
|
||
it "will return a #{expected_slug_response} when requesting a secure topic by slug" do
|
||
get "/t/#{secure_topic.slug}"
|
||
expect(response.status).to eq(expected_slug_response)
|
||
end
|
||
end
|
||
|
||
context 'without detailed error pages' do
|
||
before do
|
||
SiteSetting.detailed_404 = false
|
||
end
|
||
|
||
context 'anonymous' do
|
||
expected = {
|
||
normal_topic: 200,
|
||
secure_topic: 404,
|
||
private_topic: 404,
|
||
deleted_topic: 404,
|
||
deleted_secure_topic: 404,
|
||
deleted_private_topic: 404,
|
||
nonexist: 404,
|
||
secure_accessible_topic: 404
|
||
}
|
||
include_examples "various scenarios", expected
|
||
end
|
||
|
||
context 'anonymous with login required' do
|
||
before do
|
||
SiteSetting.login_required = true
|
||
end
|
||
expected = {
|
||
normal_topic: 302,
|
||
secure_topic: 302,
|
||
private_topic: 302,
|
||
deleted_topic: 302,
|
||
deleted_secure_topic: 302,
|
||
deleted_private_topic: 302,
|
||
nonexist: 302,
|
||
secure_accessible_topic: 302
|
||
}
|
||
include_examples "various scenarios", expected
|
||
end
|
||
|
||
context 'normal user' do
|
||
before do
|
||
sign_in(user)
|
||
end
|
||
|
||
expected = {
|
||
normal_topic: 200,
|
||
secure_topic: 404,
|
||
private_topic: 404,
|
||
deleted_topic: 404,
|
||
deleted_secure_topic: 404,
|
||
deleted_private_topic: 404,
|
||
nonexist: 404,
|
||
secure_accessible_topic: 404
|
||
}
|
||
include_examples "various scenarios", expected
|
||
end
|
||
|
||
context 'allowed user' do
|
||
before do
|
||
sign_in(allowed_user)
|
||
end
|
||
|
||
expected = {
|
||
normal_topic: 200,
|
||
secure_topic: 200,
|
||
private_topic: 200,
|
||
deleted_topic: 404,
|
||
deleted_secure_topic: 404,
|
||
deleted_private_topic: 404,
|
||
nonexist: 404,
|
||
secure_accessible_topic: 404
|
||
}
|
||
include_examples "various scenarios", expected
|
||
end
|
||
|
||
context 'moderator' do
|
||
before do
|
||
sign_in(moderator)
|
||
end
|
||
|
||
expected = {
|
||
normal_topic: 200,
|
||
secure_topic: 404,
|
||
private_topic: 404,
|
||
deleted_topic: 200,
|
||
deleted_secure_topic: 404,
|
||
deleted_private_topic: 404,
|
||
nonexist: 404,
|
||
secure_accessible_topic: 404
|
||
}
|
||
include_examples "various scenarios", expected
|
||
end
|
||
|
||
context 'admin' do
|
||
before do
|
||
sign_in(admin)
|
||
end
|
||
|
||
expected = {
|
||
normal_topic: 200,
|
||
secure_topic: 200,
|
||
private_topic: 200,
|
||
deleted_topic: 200,
|
||
deleted_secure_topic: 200,
|
||
deleted_private_topic: 200,
|
||
nonexist: 404,
|
||
secure_accessible_topic: 200
|
||
}
|
||
include_examples "various scenarios", expected
|
||
end
|
||
end
|
||
|
||
context 'with detailed error pages' do
|
||
before do
|
||
SiteSetting.detailed_404 = true
|
||
end
|
||
|
||
context 'anonymous' do
|
||
expected = {
|
||
normal_topic: 200,
|
||
secure_topic: 403,
|
||
private_topic: 403,
|
||
deleted_topic: 410,
|
||
deleted_secure_topic: 403,
|
||
deleted_private_topic: 403,
|
||
nonexist: 404,
|
||
secure_accessible_topic: 403
|
||
}
|
||
include_examples "various scenarios", expected
|
||
end
|
||
|
||
context 'anonymous with login required' do
|
||
before do
|
||
SiteSetting.login_required = true
|
||
end
|
||
expected = {
|
||
normal_topic: 302,
|
||
secure_topic: 302,
|
||
private_topic: 302,
|
||
deleted_topic: 302,
|
||
deleted_secure_topic: 302,
|
||
deleted_private_topic: 302,
|
||
nonexist: 302,
|
||
secure_accessible_topic: 302
|
||
}
|
||
include_examples "various scenarios", expected
|
||
end
|
||
|
||
context 'normal user' do
|
||
before do
|
||
sign_in(user)
|
||
end
|
||
|
||
expected = {
|
||
normal_topic: 200,
|
||
secure_topic: 403,
|
||
private_topic: 403,
|
||
deleted_topic: 410,
|
||
deleted_secure_topic: 403,
|
||
deleted_private_topic: 403,
|
||
nonexist: 404,
|
||
secure_accessible_topic: 403
|
||
}
|
||
include_examples "various scenarios", expected
|
||
end
|
||
|
||
context 'allowed user' do
|
||
before do
|
||
sign_in(allowed_user)
|
||
end
|
||
|
||
expected = {
|
||
normal_topic: 200,
|
||
secure_topic: 200,
|
||
private_topic: 200,
|
||
deleted_topic: 410,
|
||
deleted_secure_topic: 410,
|
||
deleted_private_topic: 410,
|
||
nonexist: 404,
|
||
secure_accessible_topic: 403
|
||
}
|
||
include_examples "various scenarios", expected
|
||
end
|
||
|
||
context 'moderator' do
|
||
before do
|
||
sign_in(moderator)
|
||
end
|
||
|
||
expected = {
|
||
normal_topic: 200,
|
||
secure_topic: 403,
|
||
private_topic: 403,
|
||
deleted_topic: 200,
|
||
deleted_secure_topic: 403,
|
||
deleted_private_topic: 403,
|
||
nonexist: 404,
|
||
secure_accessible_topic: 403
|
||
}
|
||
include_examples "various scenarios", expected
|
||
end
|
||
|
||
context 'admin' do
|
||
before do
|
||
sign_in(admin)
|
||
end
|
||
|
||
expected = {
|
||
normal_topic: 200,
|
||
secure_topic: 200,
|
||
private_topic: 200,
|
||
deleted_topic: 200,
|
||
deleted_secure_topic: 200,
|
||
deleted_private_topic: 200,
|
||
nonexist: 404,
|
||
secure_accessible_topic: 200
|
||
}
|
||
include_examples "various scenarios", expected
|
||
end
|
||
end
|
||
|
||
end
|
||
|
||
it 'records a view' do
|
||
expect do
|
||
get "/t/#{topic.slug}/#{topic.id}.json"
|
||
end.to change(TopicViewItem, :count).by(1)
|
||
end
|
||
|
||
it 'records a view to invalid post_number' do
|
||
expect do
|
||
get "/t/#{topic.slug}/#{topic.id}/#{256**4}", params: {
|
||
u: user.username
|
||
}
|
||
expect(response.status).to eq(200)
|
||
end.to change { IncomingLink.count }.by(1)
|
||
|
||
end
|
||
|
||
it 'records incoming links' do
|
||
expect do
|
||
get "/t/#{topic.slug}/#{topic.id}", params: {
|
||
u: user.username
|
||
}
|
||
end.to change { IncomingLink.count }.by(1)
|
||
end
|
||
|
||
context 'print' do
|
||
it "doesn't renders the print view when disabled" do
|
||
SiteSetting.max_prints_per_hour_per_user = 0
|
||
|
||
get "/t/#{topic.slug}/#{topic.id}/print"
|
||
|
||
expect(response).to be_forbidden
|
||
end
|
||
|
||
it 'renders the print view when enabled' do
|
||
SiteSetting.max_prints_per_hour_per_user = 10
|
||
get "/t/#{topic.slug}/#{topic.id}/print", headers: { HTTP_USER_AGENT: "Rails Testing" }
|
||
|
||
expect(response.status).to eq(200)
|
||
body = response.body
|
||
|
||
expect(body).to have_tag(:body, class: 'crawler')
|
||
expect(body).to_not have_tag(:meta, with: { name: 'fragment' })
|
||
end
|
||
|
||
it "uses the application layout when there's no param" do
|
||
SiteSetting.max_prints_per_hour_per_user = 10
|
||
get "/t/#{topic.slug}/#{topic.id}", headers: { HTTP_USER_AGENT: "Rails Testing" }
|
||
|
||
body = response.body
|
||
|
||
expect(body).to have_tag(:script, src: '/assets/application.js')
|
||
expect(body).to have_tag(:meta, with: { name: 'fragment' })
|
||
end
|
||
end
|
||
|
||
it 'records redirects' do
|
||
get "/t/#{topic.id}", headers: { HTTP_REFERER: "http://twitter.com" }
|
||
get "/t/#{topic.slug}/#{topic.id}", headers: { HTTP_REFERER: nil }
|
||
|
||
link = IncomingLink.first
|
||
expect(link.referer).to eq('http://twitter.com')
|
||
end
|
||
|
||
it 'tracks a visit for all html requests' do
|
||
sign_in(user)
|
||
get "/t/#{topic.slug}/#{topic.id}"
|
||
topic_user = TopicUser.where(user: user, topic: topic).first
|
||
expect(topic_user.last_visited_at).to eq_time(topic_user.first_visited_at)
|
||
end
|
||
|
||
context 'consider for a promotion' do
|
||
before do
|
||
SiteSetting.tl1_requires_topics_entered = 0
|
||
SiteSetting.tl1_requires_read_posts = 0
|
||
SiteSetting.tl1_requires_time_spent_mins = 0
|
||
SiteSetting.tl1_requires_time_spent_mins = 0
|
||
end
|
||
|
||
it "reviews the user for a promotion if they're new" do
|
||
sign_in(user)
|
||
user.update_column(:trust_level, TrustLevel[0])
|
||
get "/t/#{topic.slug}/#{topic.id}.json"
|
||
user.reload
|
||
expect(user.trust_level).to eq(1)
|
||
end
|
||
end
|
||
|
||
context 'filters' do
|
||
def extract_post_stream
|
||
json = response.parsed_body
|
||
json["post_stream"]["posts"].map { |post| post["id"] }
|
||
end
|
||
|
||
before do
|
||
TopicView.stubs(:chunk_size).returns(2)
|
||
@post_ids = topic.posts.pluck(:id)
|
||
3.times do
|
||
@post_ids << Fabricate(:post, topic: topic).id
|
||
end
|
||
end
|
||
|
||
it 'grabs the correct set of posts' do
|
||
get "/t/#{topic.slug}/#{topic.id}.json"
|
||
expect(response.status).to eq(200)
|
||
expect(extract_post_stream).to eq(@post_ids[0..1])
|
||
|
||
get "/t/#{topic.slug}/#{topic.id}.json", params: { page: 1 }
|
||
expect(response.status).to eq(200)
|
||
expect(extract_post_stream).to eq(@post_ids[0..1])
|
||
|
||
get "/t/#{topic.slug}/#{topic.id}.json", params: { page: 2 }
|
||
expect(response.status).to eq(200)
|
||
expect(extract_post_stream).to eq(@post_ids[2..3])
|
||
|
||
post_number = topic.posts.pluck(:post_number).sort[3]
|
||
get "/t/#{topic.slug}/#{topic.id}/#{post_number}.json"
|
||
expect(response.status).to eq(200)
|
||
expect(extract_post_stream).to eq(@post_ids[-2..-1])
|
||
end
|
||
end
|
||
|
||
describe '#show filters' do
|
||
let(:post) { Fabricate(:post) }
|
||
let(:topic) { post.topic }
|
||
|
||
describe 'filter by replies to a post' do
|
||
let!(:post2) { Fabricate(:post, topic: topic) }
|
||
let!(:post3) { Fabricate(:post, topic: topic, reply_to_post_number: post2.post_number) }
|
||
let!(:post4) { Fabricate(:post, topic: topic, reply_to_post_number: post2.post_number) }
|
||
let!(:post5) { Fabricate(:post, topic: topic) }
|
||
let!(:quote_reply) { Fabricate(:basic_reply, user: user, topic: topic) }
|
||
let!(:post_reply) { PostReply.create(post_id: post2.id, reply_post_id: quote_reply.id) }
|
||
|
||
it 'should return the right posts' do
|
||
get "/t/#{topic.id}.json", params: {
|
||
replies_to_post_number: post2.post_number
|
||
}
|
||
|
||
expect(response.status).to eq(200)
|
||
|
||
body = response.parsed_body
|
||
|
||
expect(body.has_key?("suggested_topics")).to eq(false)
|
||
expect(body.has_key?("related_messages")).to eq(false)
|
||
|
||
ids = body["post_stream"]["posts"].map { |p| p["id"] }
|
||
expect(ids).to eq([post.id, post2.id, post3.id, post4.id, quote_reply.id])
|
||
end
|
||
end
|
||
|
||
describe 'filter upwards by post id' do
|
||
let!(:post2) { Fabricate(:post, topic: topic) }
|
||
let!(:post3) { Fabricate(:post, topic: topic) }
|
||
let!(:post4) { Fabricate(:post, topic: topic, reply_to_post_number: post3.post_number) }
|
||
let!(:post5) { Fabricate(:post, topic: topic, reply_to_post_number: post4.post_number) }
|
||
let!(:post6) { Fabricate(:post, topic: topic) }
|
||
|
||
it 'should return the right posts' do
|
||
get "/t/#{topic.id}.json", params: {
|
||
filter_upwards_post_id: post5.id
|
||
}
|
||
|
||
expect(response.status).to eq(200)
|
||
|
||
body = response.parsed_body
|
||
|
||
expect(body.has_key?("suggested_topics")).to eq(false)
|
||
expect(body.has_key?("related_messages")).to eq(false)
|
||
|
||
ids = body["post_stream"]["posts"].map { |p| p["id"] }
|
||
# includes topic OP, current post and subsequent posts
|
||
# but only one level of parents, respecting default max_reply_history = 1
|
||
expect(ids).to eq([post.id, post4.id, post5.id, post6.id])
|
||
end
|
||
|
||
it 'should respect max_reply_history site setting' do
|
||
SiteSetting.max_reply_history = 2
|
||
|
||
get "/t/#{topic.id}.json", params: {
|
||
filter_upwards_post_id: post5.id
|
||
}
|
||
|
||
expect(response.status).to eq(200)
|
||
|
||
body = response.parsed_body
|
||
ids = body["post_stream"]["posts"].map { |p| p["id"] }
|
||
|
||
# includes 2 levels of replies (post3 and post4)
|
||
expect(ids).to eq([post.id, post3.id, post4.id, post5.id, post6.id])
|
||
end
|
||
end
|
||
|
||
end
|
||
|
||
context "when 'login required' site setting has been enabled" do
|
||
before { SiteSetting.login_required = true }
|
||
|
||
context 'and the user is logged in' do
|
||
before { sign_in(Fabricate(:coding_horror)) }
|
||
|
||
it 'shows the topic' do
|
||
get "/t/#{topic.slug}/#{topic.id}.json"
|
||
expect(response.status).to eq(200)
|
||
end
|
||
end
|
||
|
||
context 'and the user is not logged in' do
|
||
let(:api_key) { Fabricate(:api_key, user: topic.user) }
|
||
|
||
it 'redirects to the login page' do
|
||
get "/t/#{topic.slug}/#{topic.id}.json"
|
||
|
||
expect(response).to redirect_to login_path
|
||
end
|
||
|
||
it 'shows the topic if valid api key is provided' do
|
||
get "/t/#{topic.slug}/#{topic.id}.json", headers: { "HTTP_API_KEY" => api_key.key }
|
||
|
||
expect(response.status).to eq(200)
|
||
topic.reload
|
||
expect(topic.views).to eq(1)
|
||
end
|
||
|
||
it 'returns 403 for an invalid key' do
|
||
[:json, :html].each do |format|
|
||
get "/t/#{topic.slug}/#{topic.id}.#{format}", headers: { "HTTP_API_KEY" => "bad" }
|
||
|
||
expect(response.code.to_i).to eq(403)
|
||
expect(response.body).to include(I18n.t("invalid_access"))
|
||
end
|
||
end
|
||
end
|
||
end
|
||
|
||
it "is included for unlisted topics" do
|
||
topic = Fabricate(:topic, visible: false)
|
||
get "/t/#{topic.slug}/#{topic.id}.json"
|
||
|
||
expect(response.headers['X-Robots-Tag']).to eq('noindex')
|
||
end
|
||
|
||
it "is not included for normal topics" do
|
||
get "/t/#{topic.slug}/#{topic.id}.json"
|
||
|
||
expect(response.headers['X-Robots-Tag']).to eq(nil)
|
||
end
|
||
|
||
it "is included when allow_index_in_robots_txt is set to false" do
|
||
SiteSetting.allow_index_in_robots_txt = false
|
||
|
||
get "/t/#{topic.slug}/#{topic.id}.json"
|
||
|
||
expect(response.headers['X-Robots-Tag']).to eq('noindex, nofollow')
|
||
end
|
||
|
||
it "doesn't store an incoming link when there's no referer" do
|
||
expect {
|
||
get "/t/#{topic.id}.json"
|
||
}.not_to change(IncomingLink, :count)
|
||
expect(response.status).to eq(200)
|
||
end
|
||
|
||
it "doesn't raise an error on a very long link" do
|
||
get "/t/#{topic.id}.json", headers: { HTTP_REFERER: "http://#{'a' * 2000}.com" }
|
||
expect(response.status).to eq(200)
|
||
end
|
||
|
||
describe "has_escaped_fragment?" do
|
||
context "when the SiteSetting is disabled" do
|
||
it "uses the application layout even with an escaped fragment param" do
|
||
SiteSetting.enable_escaped_fragments = false
|
||
|
||
get "/t/#{topic.slug}/#{topic.id}", params: {
|
||
_escaped_fragment_: 'true'
|
||
}
|
||
|
||
body = response.body
|
||
|
||
expect(response.status).to eq(200)
|
||
expect(body).to have_tag(:script, with: { src: '/assets/application.js' })
|
||
expect(body).to_not have_tag(:meta, with: { name: 'fragment' })
|
||
end
|
||
end
|
||
|
||
context "when the SiteSetting is enabled" do
|
||
before do
|
||
SiteSetting.enable_escaped_fragments = true
|
||
end
|
||
|
||
it "uses the application layout when there's no param" do
|
||
get "/t/#{topic.slug}/#{topic.id}"
|
||
|
||
body = response.body
|
||
|
||
expect(body).to have_tag(:script, with: { src: '/assets/application.js' })
|
||
expect(body).to have_tag(:meta, with: { name: 'fragment' })
|
||
end
|
||
|
||
it "uses the crawler layout when there's an _escaped_fragment_ param" do
|
||
get "/t/#{topic.slug}/#{topic.id}", params: {
|
||
_escaped_fragment_: true
|
||
}, headers: { HTTP_USER_AGENT: "Rails Testing" }
|
||
|
||
body = response.body
|
||
|
||
expect(response.status).to eq(200)
|
||
expect(body).to have_tag(:body, with: { class: 'crawler' })
|
||
expect(body).to_not have_tag(:meta, with: { name: 'fragment' })
|
||
end
|
||
end
|
||
end
|
||
|
||
describe 'clear_notifications' do
|
||
it 'correctly clears notifications if specified via cookie' do
|
||
set_subfolder "/eviltrout"
|
||
|
||
notification = Fabricate(:notification)
|
||
sign_in(notification.user)
|
||
|
||
cookies['cn'] = "2828,100,#{notification.id}"
|
||
|
||
get "/t/#{topic.id}.json"
|
||
|
||
expect(response.status).to eq(200)
|
||
expect(response.cookies['cn']).to eq(nil)
|
||
expect(response.headers['Set-Cookie']).to match(/^cn=;.*path=\/eviltrout/)
|
||
|
||
notification.reload
|
||
expect(notification.read).to eq(true)
|
||
end
|
||
|
||
it 'correctly clears notifications if specified via header' do
|
||
notification = Fabricate(:notification)
|
||
sign_in(notification.user)
|
||
|
||
get "/t/#{topic.id}.json", headers: { "Discourse-Clear-Notifications" => "2828,100,#{notification.id}" }
|
||
|
||
expect(response.status).to eq(200)
|
||
notification.reload
|
||
expect(notification.read).to eq(true)
|
||
end
|
||
end
|
||
|
||
describe "read only header" do
|
||
it "returns no read only header by default" do
|
||
get "/t/#{topic.id}.json"
|
||
expect(response.status).to eq(200)
|
||
expect(response.headers['Discourse-Readonly']).to eq(nil)
|
||
end
|
||
|
||
it "returns a readonly header if the site is read only" do
|
||
Discourse.received_postgres_readonly!
|
||
get "/t/#{topic.id}.json"
|
||
expect(response.status).to eq(200)
|
||
expect(response.headers['Discourse-Readonly']).to eq('true')
|
||
end
|
||
end
|
||
|
||
describe "image only topic" do
|
||
it "uses image alt tag for meta description" do
|
||
post = Fabricate(:post, raw: "![image_description|690x405](upload://sdtr5O5xaxf0iEOxICxL36YRj86.png)")
|
||
|
||
get post.topic.url
|
||
|
||
body = response.body
|
||
expect(body).to have_tag(:meta, with: { name: 'description', content: '[image_description]' })
|
||
end
|
||
end
|
||
end
|
||
|
||
describe '#post_ids' do
|
||
let(:post) { Fabricate(:post) }
|
||
let(:topic) { post.topic }
|
||
|
||
before do
|
||
TopicView.stubs(:chunk_size).returns(1)
|
||
end
|
||
|
||
it 'returns the right post ids' do
|
||
post2 = Fabricate(:post, topic: topic)
|
||
post3 = Fabricate(:post, topic: topic)
|
||
|
||
get "/t/#{topic.id}/post_ids.json", params: {
|
||
post_number: post.post_number
|
||
}
|
||
|
||
expect(response.status).to eq(200)
|
||
|
||
body = response.parsed_body
|
||
|
||
expect(body["post_ids"]).to eq([post2.id, post3.id])
|
||
end
|
||
|
||
describe 'filtering by post number with filters' do
|
||
describe 'username filters' do
|
||
let(:post) { Fabricate(:post, user: user) }
|
||
let!(:post2) { Fabricate(:post, topic: topic, user: user) }
|
||
let!(:post3) { Fabricate(:post, topic: topic) }
|
||
|
||
it 'should return the right posts' do
|
||
get "/t/#{topic.id}/post_ids.json", params: {
|
||
post_number: post.post_number,
|
||
username_filters: post2.user.username
|
||
}
|
||
|
||
expect(response.status).to eq(200)
|
||
|
||
body = response.parsed_body
|
||
|
||
expect(body["post_ids"]).to eq([post2.id])
|
||
end
|
||
end
|
||
|
||
describe 'summary filter' do
|
||
let!(:post2) { Fabricate(:post, topic: topic, percent_rank: 0.2) }
|
||
let!(:post3) { Fabricate(:post, topic: topic) }
|
||
|
||
it 'should return the right posts' do
|
||
get "/t/#{topic.id}/post_ids.json", params: {
|
||
post_number: post.post_number,
|
||
filter: 'summary'
|
||
}
|
||
|
||
expect(response.status).to eq(200)
|
||
|
||
body = response.parsed_body
|
||
|
||
expect(body["post_ids"]).to eq([post2.id])
|
||
end
|
||
end
|
||
end
|
||
end
|
||
|
||
describe '#posts' do
|
||
let(:post) { Fabricate(:post) }
|
||
let(:topic) { post.topic }
|
||
|
||
after do
|
||
Discourse.redis.flushdb
|
||
end
|
||
|
||
it 'returns first post of the topic' do
|
||
# we need one for suggested
|
||
create_post
|
||
|
||
get "/t/#{topic.id}/posts.json"
|
||
|
||
expect(response.status).to eq(200)
|
||
|
||
body = response.parsed_body
|
||
|
||
expect(body["post_stream"]["posts"].first["id"]).to eq(post.id)
|
||
|
||
expect(body["suggested_topics"]).to eq(nil)
|
||
|
||
get "/t/#{topic.id}/posts.json?include_suggested=true"
|
||
body = response.parsed_body
|
||
|
||
expect(body["suggested_topics"]).not_to eq(nil)
|
||
end
|
||
|
||
describe 'filtering by post number with filters' do
|
||
describe 'username filters' do
|
||
let!(:post2) { Fabricate(:post, topic: topic, user: user) }
|
||
let!(:post3) { Fabricate(:post, topic: topic) }
|
||
|
||
it 'should return the right posts' do
|
||
TopicView.stubs(:chunk_size).returns(2)
|
||
|
||
get "/t/#{topic.id}/posts.json", params: {
|
||
post_number: post.post_number,
|
||
username_filters: post2.user.username,
|
||
asc: true
|
||
}
|
||
|
||
expect(response.status).to eq(200)
|
||
|
||
body = response.parsed_body
|
||
|
||
expect(body["post_stream"]["posts"].first["id"]).to eq(post2.id)
|
||
end
|
||
end
|
||
|
||
describe 'summary filter' do
|
||
let!(:post2) { Fabricate(:post, topic: topic, percent_rank: 0.2) }
|
||
let!(:post3) { Fabricate(:post, topic: topic) }
|
||
|
||
it 'should return the right posts' do
|
||
TopicView.stubs(:chunk_size).returns(2)
|
||
|
||
get "/t/#{topic.id}/posts.json", params: {
|
||
post_number: post.post_number,
|
||
filter: 'summary',
|
||
asc: true
|
||
}
|
||
|
||
expect(response.status).to eq(200)
|
||
|
||
body = response.parsed_body
|
||
|
||
expect(body["post_stream"]["posts"].first["id"]).to eq(post2.id)
|
||
end
|
||
end
|
||
end
|
||
end
|
||
|
||
describe '#feed' do
|
||
let(:topic) { Fabricate(:post).topic }
|
||
|
||
it 'renders rss of the topic' do
|
||
get "/t/foo/#{topic.id}.rss"
|
||
expect(response.status).to eq(200)
|
||
expect(response.media_type).to eq('application/rss+xml')
|
||
end
|
||
|
||
it 'renders rss of the topic correctly with subfolder' do
|
||
set_subfolder "/forum"
|
||
get "/t/foo/#{topic.id}.rss"
|
||
expect(response.status).to eq(200)
|
||
expect(response.body).to_not include("/forum/forum")
|
||
expect(response.body).to include("http://test.localhost/forum/t/#{topic.slug}")
|
||
end
|
||
|
||
it 'returns 404 when posts are deleted' do
|
||
topic.posts.each(&:trash!)
|
||
get "/t/foo/#{topic.id}.rss"
|
||
expect(response.status).to eq(404)
|
||
end
|
||
end
|
||
|
||
describe '#invite_group' do
|
||
let(:admins) { Group[:admins] }
|
||
|
||
before do
|
||
sign_in(admin)
|
||
admins.messageable_level = Group::ALIAS_LEVELS[:everyone]
|
||
admins.save!
|
||
end
|
||
|
||
it "disallows inviting a group to a topic" do
|
||
topic = Fabricate(:topic)
|
||
post "/t/#{topic.id}/invite-group.json", params: {
|
||
group: 'admins'
|
||
}
|
||
|
||
expect(response.status).to eq(422)
|
||
end
|
||
|
||
it "allows inviting a group to a PM" do
|
||
topic = Fabricate(:private_message_topic)
|
||
post "/t/#{topic.id}/invite-group.json", params: {
|
||
group: 'admins'
|
||
}
|
||
|
||
expect(response.status).to eq(200)
|
||
expect(topic.allowed_groups.first.id).to eq(admins.id)
|
||
end
|
||
end
|
||
|
||
describe '#make_banner' do
|
||
it 'needs you to be a staff member' do
|
||
topic = Fabricate(:topic, user: sign_in(trust_level_4))
|
||
put "/t/#{topic.id}/make-banner.json"
|
||
expect(response).to be_forbidden
|
||
end
|
||
|
||
describe 'when logged in' do
|
||
it "changes the topic archetype to 'banner'" do
|
||
topic = Fabricate(:topic, user: sign_in(admin))
|
||
|
||
put "/t/#{topic.id}/make-banner.json"
|
||
expect(response.status).to eq(200)
|
||
topic.reload
|
||
expect(topic.archetype).to eq(Archetype.banner)
|
||
end
|
||
end
|
||
end
|
||
|
||
describe '#remove_banner' do
|
||
it 'needs you to be a staff member' do
|
||
topic = Fabricate(:topic, user: sign_in(trust_level_4), archetype: Archetype.banner)
|
||
put "/t/#{topic.id}/remove-banner.json"
|
||
expect(response).to be_forbidden
|
||
end
|
||
|
||
describe 'when logged in' do
|
||
it "resets the topic archetype" do
|
||
topic = Fabricate(:topic, user: sign_in(admin), archetype: Archetype.banner)
|
||
|
||
put "/t/#{topic.id}/remove-banner.json"
|
||
expect(response.status).to eq(200)
|
||
topic.reload
|
||
expect(topic.archetype).to eq(Archetype.default)
|
||
end
|
||
end
|
||
end
|
||
|
||
describe '#remove_allowed_user' do
|
||
it 'admin can be removed from a pm' do
|
||
sign_in(admin)
|
||
pm = create_post(user: user, archetype: 'private_message', target_usernames: [user.username, admin.username])
|
||
|
||
put "/t/#{pm.topic_id}/remove-allowed-user.json", params: {
|
||
username: admin.username
|
||
}
|
||
|
||
expect(response.status).to eq(200)
|
||
expect(TopicAllowedUser.where(topic_id: pm.topic_id, user_id: admin.id).first).to eq(nil)
|
||
end
|
||
end
|
||
|
||
describe '#bulk' do
|
||
it 'needs you to be logged in' do
|
||
put "/topics/bulk.json"
|
||
expect(response.status).to eq(403)
|
||
end
|
||
|
||
describe "when logged in" do
|
||
before { sign_in(user) }
|
||
let(:operation) { { type: 'change_category', category_id: '1' } }
|
||
let(:topic_ids) { [1, 2, 3] }
|
||
|
||
it "requires a list of topic_ids or filter" do
|
||
put "/topics/bulk.json", params: { operation: operation }
|
||
expect(response.status).to eq(400)
|
||
end
|
||
|
||
it "requires an operation param" do
|
||
put "/topics/bulk.json", params: { topic_ids: topic_ids }
|
||
expect(response.status).to eq(400)
|
||
end
|
||
|
||
it "requires a type field for the operation param" do
|
||
put "/topics/bulk.json", params: { topic_ids: topic_ids, operation: {} }
|
||
expect(response.status).to eq(400)
|
||
end
|
||
|
||
it "can mark sub-categories unread" do
|
||
category = Fabricate(:category)
|
||
sub = Fabricate(:category, parent_category_id: category.id)
|
||
|
||
topic.update!(category_id: sub.id)
|
||
|
||
post1 = create_post(user: user, topic_id: topic.id)
|
||
create_post(topic_id: topic.id)
|
||
|
||
put "/topics/bulk.json", params: {
|
||
category_id: category.id,
|
||
include_subcategories: true,
|
||
filter: 'unread',
|
||
operation: { type: 'dismiss_posts' }
|
||
}
|
||
|
||
expect(response.status).to eq(200)
|
||
expect(TopicUser.get(post1.topic, post1.user).last_read_post_number).to eq(2)
|
||
end
|
||
|
||
it "can mark tag topics unread" do
|
||
tag = Fabricate(:tag)
|
||
TopicTag.create!(
|
||
topic_id: topic.id,
|
||
tag_id: tag.id
|
||
)
|
||
|
||
post1 = create_post(user: user, topic_id: topic.id)
|
||
create_post(topic_id: topic.id)
|
||
|
||
put "/topics/bulk.json", params: {
|
||
tag_name: tag.name,
|
||
filter: 'unread',
|
||
operation: { type: 'dismiss_posts' }
|
||
}
|
||
|
||
expect(response.status).to eq(200)
|
||
expect(TopicUser.get(post1.topic, post1.user).last_read_post_number).to eq(2)
|
||
end
|
||
|
||
it "can find unread" do
|
||
# mark all unread muted
|
||
put "/topics/bulk.json", params: {
|
||
filter: 'unread', operation: { type: :change_notification_level, notification_level_id: 0 }
|
||
}
|
||
|
||
expect(response.status).to eq(200)
|
||
end
|
||
|
||
it "delegates work to `TopicsBulkAction`" do
|
||
topics_bulk_action = mock
|
||
TopicsBulkAction.expects(:new).with(user, topic_ids, operation, group: nil).returns(topics_bulk_action)
|
||
topics_bulk_action.expects(:perform!)
|
||
|
||
put "/topics/bulk.json", params: {
|
||
topic_ids: topic_ids, operation: operation
|
||
}
|
||
end
|
||
|
||
it "respects the tracked parameter" do
|
||
# untracked topic
|
||
category = Fabricate(:category)
|
||
CategoryUser.set_notification_level_for_category(user,
|
||
NotificationLevels.all[:regular],
|
||
category.id)
|
||
create_post(user: user, topic_id: topic.id)
|
||
topic.update!(category_id: category.id)
|
||
create_post(topic_id: topic.id)
|
||
|
||
# tracked topic
|
||
tracked_category = Fabricate(:category)
|
||
CategoryUser.set_notification_level_for_category(user,
|
||
NotificationLevels.all[:tracking],
|
||
tracked_category.id)
|
||
tracked_topic = create_post(user: user).topic
|
||
tracked_topic.update!(category_id: tracked_category.id)
|
||
create_post(topic_id: tracked_topic.id)
|
||
|
||
put "/topics/bulk.json", params: {
|
||
filter: 'unread',
|
||
operation: { type: 'dismiss_posts' },
|
||
tracked: true
|
||
}
|
||
|
||
expect(response.status).to eq(200)
|
||
expect(TopicUser.get(topic, user).last_read_post_number).to eq(topic.posts.count - 1)
|
||
expect(TopicUser.get(tracked_topic, user).last_read_post_number).to eq(tracked_topic.posts.count)
|
||
end
|
||
end
|
||
end
|
||
|
||
describe '#remove_bookmarks' do
|
||
it "should remove bookmarks properly from non first post" do
|
||
sign_in(user)
|
||
|
||
post = create_post
|
||
post2 = create_post(topic_id: post.topic_id)
|
||
Fabricate(:bookmark, user: user, post: post)
|
||
Fabricate(:bookmark, user: user, post: post2)
|
||
|
||
put "/t/#{post.topic_id}/remove_bookmarks.json"
|
||
expect(Bookmark.where(user: user).count).to eq(0)
|
||
end
|
||
|
||
it "should disallow bookmarks on posts you have no access to" do
|
||
sign_in(Fabricate(:user))
|
||
pm = create_post(user: user, archetype: 'private_message', target_usernames: [user.username])
|
||
|
||
put "/t/#{pm.topic_id}/bookmark.json"
|
||
expect(response).to be_forbidden
|
||
end
|
||
|
||
context "bookmarks with reminders" do
|
||
it "deletes all the bookmarks for the user in the topic" do
|
||
sign_in(user)
|
||
post = create_post
|
||
Fabricate(:bookmark, post: post, topic: post.topic, user: user)
|
||
put "/t/#{post.topic_id}/remove_bookmarks.json"
|
||
expect(Bookmark.where(user: user, topic: topic).count).to eq(0)
|
||
end
|
||
end
|
||
end
|
||
|
||
describe "#bookmark" do
|
||
before do
|
||
sign_in(user)
|
||
end
|
||
|
||
it "should create a new bookmark on the first post of the topic" do
|
||
post = create_post
|
||
post2 = create_post(topic_id: post.topic_id)
|
||
put "/t/#{post.topic_id}/bookmark.json"
|
||
|
||
expect(Bookmark.find_by(user_id: user.id).post_id).to eq(post.id)
|
||
end
|
||
|
||
it "errors if the topic is already bookmarked for the user" do
|
||
post = create_post
|
||
Bookmark.create(post: post, user: user, topic: post.topic)
|
||
|
||
put "/t/#{post.topic_id}/bookmark.json"
|
||
expect(response.status).to eq(400)
|
||
end
|
||
|
||
context "bookmarks with reminders" do
|
||
it "should create a new bookmark on the first post of the topic" do
|
||
post = create_post
|
||
post2 = create_post(topic_id: post.topic_id)
|
||
put "/t/#{post.topic_id}/bookmark.json"
|
||
expect(response.status).to eq(200)
|
||
|
||
bookmarks_for_topic = Bookmark.where(topic: post.topic, user: user)
|
||
expect(bookmarks_for_topic.count).to eq(1)
|
||
expect(bookmarks_for_topic.first.post_id).to eq(post.id)
|
||
end
|
||
|
||
it "errors if the topic is already bookmarked for the user" do
|
||
post = create_post
|
||
Bookmark.create(post: post, topic: post.topic, user: user)
|
||
|
||
put "/t/#{post.topic_id}/bookmark.json"
|
||
expect(response.status).to eq(400)
|
||
end
|
||
end
|
||
end
|
||
|
||
describe '#reset_new' do
|
||
it 'needs you to be logged in' do
|
||
put "/topics/reset-new.json"
|
||
expect(response.status).to eq(403)
|
||
end
|
||
|
||
it "updates the `new_since` date" do
|
||
sign_in(user)
|
||
|
||
old_date = 2.years.ago
|
||
user.user_stat.update_column(:new_since, old_date)
|
||
|
||
TopicTrackingState.expects(:publish_dismiss_new).with(user.id)
|
||
|
||
put "/topics/reset-new.json"
|
||
expect(response.status).to eq(200)
|
||
user.reload
|
||
expect(user.user_stat.new_since.to_date).not_to eq(old_date.to_date)
|
||
end
|
||
|
||
describe "when tracked param is true" do
|
||
it "does not update user_stat.new_since" do
|
||
sign_in(user)
|
||
|
||
old_date = 2.years.ago
|
||
user.user_stat.update_column(:new_since, old_date)
|
||
|
||
put "/topics/reset-new.json?tracked=true"
|
||
expect(response.status).to eq(200)
|
||
user.reload
|
||
expect(user.user_stat.new_since.to_date).to eq(old_date.to_date)
|
||
end
|
||
|
||
it "creates topic user records for each unread topic" do
|
||
sign_in(user)
|
||
user.user_stat.update_column(:new_since, 2.years.ago)
|
||
|
||
tracked_category = Fabricate(:category)
|
||
CategoryUser.set_notification_level_for_category(user,
|
||
NotificationLevels.all[:tracking],
|
||
tracked_category.id)
|
||
tracked_topic = create_post.topic
|
||
tracked_topic.update!(category_id: tracked_category.id)
|
||
|
||
create_post # This is a new post, but is not tracked so a record will not be created for it
|
||
expect { put "/topics/reset-new.json?tracked=true" }.to change { TopicUser.where(user_id: user.id, last_read_post_number: 0).count }.by(1)
|
||
end
|
||
end
|
||
|
||
context 'category' do
|
||
fab!(:category) { Fabricate(:category) }
|
||
fab!(:subcategory) { Fabricate(:category, parent_category_id: category.id) }
|
||
|
||
it 'updates last_seen_at for main category' do
|
||
sign_in(user)
|
||
category_user = CategoryUser.create!(category_id: category.id, user_id: user.id)
|
||
subcategory_user = CategoryUser.create!(category_id: subcategory.id, user_id: user.id)
|
||
|
||
TopicTrackingState.expects(:publish_dismiss_new).with(user.id, category.id.to_s)
|
||
|
||
put "/topics/reset-new.json?category_id=#{category.id}"
|
||
|
||
expect(category_user.reload.last_seen_at).not_to be_nil
|
||
expect(subcategory_user.reload.last_seen_at).to be_nil
|
||
end
|
||
|
||
it 'updates last_seen_at for main category and subcategories' do
|
||
sign_in(user)
|
||
category_user = CategoryUser.create!(category_id: category.id, user_id: user.id)
|
||
subcategory_user = CategoryUser.create!(category_id: subcategory.id, user_id: user.id)
|
||
put "/topics/reset-new.json?category_id=#{category.id}&include_subcategories=true"
|
||
|
||
expect(category_user.reload.last_seen_at).not_to be_nil
|
||
expect(subcategory_user.reload.last_seen_at).not_to be_nil
|
||
end
|
||
end
|
||
end
|
||
|
||
describe '#feature_stats' do
|
||
it "works" do
|
||
get "/topics/feature_stats.json", params: { category_id: 1 }
|
||
|
||
expect(response.status).to eq(200)
|
||
json = response.parsed_body
|
||
expect(json["pinned_in_category_count"]).to eq(0)
|
||
expect(json["pinned_globally_count"]).to eq(0)
|
||
expect(json["banner_count"]).to eq(0)
|
||
end
|
||
|
||
it "allows unlisted banner topic" do
|
||
Fabricate(:topic, category_id: 1, archetype: Archetype.banner, visible: false)
|
||
|
||
get "/topics/feature_stats.json", params: { category_id: 1 }
|
||
json = response.parsed_body
|
||
expect(json["banner_count"]).to eq(1)
|
||
end
|
||
end
|
||
|
||
describe '#excerpts' do
|
||
it "can correctly get excerpts" do
|
||
first_post = create_post(raw: 'This is the first post :)', title: 'This is a test title I am making yay')
|
||
second_post = create_post(raw: 'This is second post', topic: first_post.topic)
|
||
|
||
random_post = Fabricate(:post)
|
||
|
||
get "/t/#{first_post.topic_id}/excerpts.json", params: {
|
||
post_ids: [first_post.id, second_post.id, random_post.id]
|
||
}
|
||
|
||
json = response.parsed_body
|
||
json.sort! { |a, b| a["post_id"] <=> b["post_id"] }
|
||
|
||
# no random post
|
||
expect(json.length).to eq(2)
|
||
# keep emoji images
|
||
expect(json[0]["excerpt"]).to match(/emoji/)
|
||
expect(json[0]["excerpt"]).to match(/first post/)
|
||
expect(json[0]["username"]).to eq(first_post.user.username)
|
||
expect(json[0]["post_id"]).to eq(first_post.id)
|
||
|
||
expect(json[1]["excerpt"]).to match(/second post/)
|
||
end
|
||
end
|
||
|
||
describe '#convert_topic' do
|
||
it 'needs you to be logged in' do
|
||
put "/t/111/convert-topic/private.json"
|
||
expect(response.status).to eq(403)
|
||
end
|
||
|
||
describe 'converting public topic to private message' do
|
||
let(:topic) { Fabricate(:topic, user: user) }
|
||
let!(:post) { Fabricate(:post, topic: topic) }
|
||
|
||
it "raises an error when the user doesn't have permission to convert topic" do
|
||
sign_in(user)
|
||
put "/t/#{topic.id}/convert-topic/private.json"
|
||
expect(response).to be_forbidden
|
||
end
|
||
|
||
context "success" do
|
||
it "returns success" do
|
||
sign_in(admin)
|
||
put "/t/#{topic.id}/convert-topic/private.json"
|
||
|
||
topic.reload
|
||
expect(topic.archetype).to eq(Archetype.private_message)
|
||
expect(response.status).to eq(200)
|
||
|
||
result = response.parsed_body
|
||
expect(result['success']).to eq(true)
|
||
expect(result['url']).to be_present
|
||
end
|
||
end
|
||
end
|
||
|
||
describe 'converting private message to public topic' do
|
||
let(:topic) { Fabricate(:private_message_topic, user: user) }
|
||
let!(:post) { Fabricate(:post, topic: topic) }
|
||
|
||
it "raises an error when the user doesn't have permission to convert topic" do
|
||
sign_in(user)
|
||
put "/t/#{topic.id}/convert-topic/public.json"
|
||
expect(response).to be_forbidden
|
||
end
|
||
|
||
context "success" do
|
||
fab!(:category) { Fabricate(:category) }
|
||
|
||
it "returns success" do
|
||
sign_in(admin)
|
||
put "/t/#{topic.id}/convert-topic/public.json?category_id=#{category.id}"
|
||
|
||
topic.reload
|
||
expect(topic.archetype).to eq(Archetype.default)
|
||
expect(topic.category_id).to eq(category.id)
|
||
expect(response.status).to eq(200)
|
||
|
||
result = response.parsed_body
|
||
expect(result['success']).to eq(true)
|
||
expect(result['url']).to be_present
|
||
end
|
||
end
|
||
end
|
||
end
|
||
|
||
describe '#timings' do
|
||
let(:post_1) { Fabricate(:post, topic: topic) }
|
||
|
||
it 'should record the timing' do
|
||
sign_in(user)
|
||
|
||
post "/topics/timings.json", params: {
|
||
topic_id: topic.id,
|
||
topic_time: 5,
|
||
timings: { post_1.post_number => 2 }
|
||
}
|
||
|
||
expect(response.status).to eq(200)
|
||
|
||
post_timing = PostTiming.first
|
||
|
||
expect(post_timing.topic).to eq(topic)
|
||
expect(post_timing.user).to eq(user)
|
||
expect(post_timing.msecs).to eq(2)
|
||
end
|
||
end
|
||
|
||
describe '#timer' do
|
||
context 'when a user is not logged in' do
|
||
it 'should return the right response' do
|
||
post "/t/#{topic.id}/timer.json", params: {
|
||
time: '24',
|
||
status_type: TopicTimer.types[1]
|
||
}
|
||
expect(response.status).to eq(403)
|
||
end
|
||
end
|
||
|
||
context 'when does not have permission' do
|
||
it 'should return the right response' do
|
||
sign_in(user)
|
||
|
||
post "/t/#{topic.id}/timer.json", params: {
|
||
time: '24',
|
||
status_type: TopicTimer.types[1]
|
||
}
|
||
|
||
expect(response.status).to eq(403)
|
||
expect(response.parsed_body["error_type"]).to eq('invalid_access')
|
||
end
|
||
end
|
||
|
||
context 'when time is in the past' do
|
||
it 'returns an error' do
|
||
freeze_time
|
||
sign_in(admin)
|
||
|
||
post "/t/#{topic.id}/timer.json", params: {
|
||
time: Time.current - 1.day,
|
||
status_type: TopicTimer.types[1]
|
||
}
|
||
expect(response.status).to eq(400)
|
||
end
|
||
end
|
||
|
||
context 'when logged in as an admin' do
|
||
before do
|
||
freeze_time
|
||
sign_in(admin)
|
||
end
|
||
|
||
it 'should be able to create a topic status update' do
|
||
post "/t/#{topic.id}/timer.json", params: {
|
||
time: 24,
|
||
status_type: TopicTimer.types[1]
|
||
}
|
||
|
||
expect(response.status).to eq(200)
|
||
|
||
topic_status_update = TopicTimer.last
|
||
|
||
expect(topic_status_update.topic).to eq(topic)
|
||
expect(topic_status_update.execute_at).to eq_time(24.hours.from_now)
|
||
|
||
json = response.parsed_body
|
||
|
||
expect(DateTime.parse(json['execute_at']))
|
||
.to eq_time(DateTime.parse(topic_status_update.execute_at.to_s))
|
||
|
||
expect(json['duration']).to eq(topic_status_update.duration)
|
||
expect(json['closed']).to eq(topic.reload.closed)
|
||
end
|
||
|
||
it 'should be able to delete a topic status update' do
|
||
Fabricate(:topic_timer, topic: topic)
|
||
|
||
post "/t/#{topic.id}/timer.json", params: {
|
||
time: nil,
|
||
status_type: TopicTimer.types[1]
|
||
}
|
||
|
||
expect(response.status).to eq(200)
|
||
expect(topic.reload.public_topic_timer).to eq(nil)
|
||
|
||
json = response.parsed_body
|
||
|
||
expect(json['execute_at']).to eq(nil)
|
||
expect(json['duration']).to eq(nil)
|
||
expect(json['closed']).to eq(topic.closed)
|
||
end
|
||
|
||
it 'should be able to create a topic status update with duration' do
|
||
post "/t/#{topic.id}/timer.json", params: {
|
||
duration: 5,
|
||
status_type: TopicTimer.types[7]
|
||
}
|
||
|
||
expect(response.status).to eq(200)
|
||
|
||
topic_status_update = TopicTimer.last
|
||
|
||
expect(topic_status_update.topic).to eq(topic)
|
||
expect(topic_status_update.execute_at).to eq_time(5.days.from_now)
|
||
expect(topic_status_update.duration).to eq(5)
|
||
|
||
json = response.parsed_body
|
||
|
||
expect(DateTime.parse(json['execute_at']))
|
||
.to eq_time(DateTime.parse(topic_status_update.execute_at.to_s))
|
||
|
||
expect(json['duration']).to eq(topic_status_update.duration)
|
||
end
|
||
|
||
it 'should be able to delete a topic status update for delete_replies type' do
|
||
Fabricate(:topic_timer, topic: topic, status_type: TopicTimer.types[:delete_replies])
|
||
|
||
post "/t/#{topic.id}/timer.json", params: {
|
||
time: nil,
|
||
status_type: TopicTimer.types[7]
|
||
}
|
||
|
||
expect(response.status).to eq(200)
|
||
expect(topic.reload.public_topic_timer).to eq(nil)
|
||
|
||
json = response.parsed_body
|
||
|
||
expect(json['execute_at']).to eq(nil)
|
||
expect(json['duration']).to eq(nil)
|
||
expect(json['closed']).to eq(topic.closed)
|
||
end
|
||
|
||
describe 'publishing topic to category in the future' do
|
||
it 'should be able to create the topic status update' do
|
||
post "/t/#{topic.id}/timer.json", params: {
|
||
time: 24,
|
||
status_type: TopicTimer.types[3],
|
||
category_id: topic.category_id
|
||
}
|
||
|
||
expect(response.status).to eq(200)
|
||
|
||
topic_status_update = TopicTimer.last
|
||
|
||
expect(topic_status_update.topic).to eq(topic)
|
||
expect(topic_status_update.execute_at).to eq_time(24.hours.from_now)
|
||
expect(topic_status_update.status_type)
|
||
.to eq(TopicTimer.types[:publish_to_category])
|
||
|
||
json = response.parsed_body
|
||
|
||
expect(json['category_id']).to eq(topic.category_id)
|
||
end
|
||
end
|
||
|
||
describe 'invalid status type' do
|
||
it 'should raise the right error' do
|
||
post "/t/#{topic.id}/timer.json", params: {
|
||
time: 10,
|
||
status_type: 'something'
|
||
}
|
||
expect(response.status).to eq(400)
|
||
expect(response.body).to include('status_type')
|
||
end
|
||
end
|
||
end
|
||
|
||
context 'when logged in as a TL4 user' do
|
||
it "raises an error if the user can't see the topic" do
|
||
user.update!(trust_level: TrustLevel[4])
|
||
sign_in(user)
|
||
|
||
pm_topic = Fabricate(:private_message_topic)
|
||
|
||
post "/t/#{pm_topic.id}/timer.json", params: {
|
||
time: '24',
|
||
status_type: TopicTimer.types[1]
|
||
}
|
||
|
||
expect(response.status).to eq(403)
|
||
expect(response.parsed_body["error_type"]).to eq('invalid_access')
|
||
end
|
||
end
|
||
end
|
||
|
||
describe '#set_slow_mode' do
|
||
context 'when not logged in' do
|
||
it 'returns a forbidden response' do
|
||
put "/t/#{topic.id}/slow_mode.json", params: {
|
||
seconds: '3600'
|
||
}
|
||
|
||
expect(response.status).to eq(403)
|
||
end
|
||
end
|
||
|
||
context 'logged in as an admin' do
|
||
it 'allows admins to set the slow mode interval' do
|
||
sign_in(admin)
|
||
|
||
put "/t/#{topic.id}/slow_mode.json", params: {
|
||
seconds: '3600'
|
||
}
|
||
|
||
topic.reload
|
||
expect(response.status).to eq(200)
|
||
expect(topic.slow_mode_seconds).to eq(3600)
|
||
end
|
||
end
|
||
|
||
context 'logged in as a regular user' do
|
||
it 'does nothing if the user is not TL4' do
|
||
user.update!(trust_level: TrustLevel[3])
|
||
sign_in(user)
|
||
|
||
put "/t/#{topic.id}/slow_mode.json", params: {
|
||
seconds: '3600'
|
||
}
|
||
|
||
expect(response.status).to eq(403)
|
||
end
|
||
|
||
it 'allows TL4 users to set the slow mode interval' do
|
||
user.update!(trust_level: TrustLevel[4])
|
||
sign_in(user)
|
||
|
||
put "/t/#{topic.id}/slow_mode.json", params: {
|
||
seconds: '3600'
|
||
}
|
||
|
||
topic.reload
|
||
expect(response.status).to eq(200)
|
||
expect(topic.slow_mode_seconds).to eq(3600)
|
||
end
|
||
end
|
||
|
||
context 'auto-disable slow mode' do
|
||
before { sign_in(admin) }
|
||
|
||
let(:timestamp) { 1.week.from_now.to_formatted_s(:iso8601) }
|
||
|
||
it 'sets a topic timer to clear the slow mode automatically' do
|
||
put "/t/#{topic.id}/slow_mode.json", params: {
|
||
seconds: '3600', enabled_until: timestamp
|
||
}
|
||
|
||
created_timer = TopicTimer.find_by(topic: topic)
|
||
execute_at = created_timer.execute_at.to_formatted_s(:iso8601)
|
||
|
||
expect(execute_at).to eq(timestamp)
|
||
end
|
||
|
||
it 'deletes the topic timer' do
|
||
put "/t/#{topic.id}/slow_mode.json", params: {
|
||
seconds: '3600', enabled_until: timestamp
|
||
}
|
||
|
||
put "/t/#{topic.id}/slow_mode.json", params: {
|
||
seconds: '0', enabled_until: timestamp
|
||
}
|
||
|
||
created_timer = TopicTimer.find_by(topic: topic)
|
||
|
||
expect(created_timer).to be_nil
|
||
end
|
||
|
||
it 'updates the existing timer' do
|
||
put "/t/#{topic.id}/slow_mode.json", params: {
|
||
seconds: '3600', enabled_until: timestamp
|
||
}
|
||
|
||
updated_timestamp = 1.hour.from_now.to_formatted_s(:iso8601)
|
||
|
||
put "/t/#{topic.id}/slow_mode.json", params: {
|
||
seconds: '3600', enabled_until: updated_timestamp
|
||
}
|
||
|
||
created_timer = TopicTimer.find_by(topic: topic)
|
||
execute_at = created_timer.execute_at.to_formatted_s(:iso8601)
|
||
|
||
expect(execute_at).to eq(updated_timestamp)
|
||
end
|
||
end
|
||
end
|
||
|
||
describe '#invite' do
|
||
describe 'when not logged in' do
|
||
it "should return the right response" do
|
||
post "/t/#{topic.id}/invite.json", params: {
|
||
email: 'jake@adventuretime.ooo'
|
||
}
|
||
|
||
expect(response.status).to eq(403)
|
||
end
|
||
end
|
||
|
||
describe 'when logged in' do
|
||
before do
|
||
sign_in(user)
|
||
end
|
||
|
||
describe 'as a valid user' do
|
||
let(:topic) { Fabricate(:topic, user: user) }
|
||
|
||
it 'should return the right response' do
|
||
user.update!(trust_level: TrustLevel[2])
|
||
|
||
expect do
|
||
post "/t/#{topic.id}/invite.json", params: {
|
||
email: 'someguy@email.com'
|
||
}
|
||
end.to change { Invite.where(invited_by_id: user.id).count }.by(1)
|
||
|
||
expect(response.status).to eq(200)
|
||
end
|
||
end
|
||
|
||
describe 'when user is a group manager' do
|
||
let(:group) { Fabricate(:group).tap { |g| g.add_owner(user) } }
|
||
let(:private_category) { Fabricate(:private_category, group: group) }
|
||
|
||
let(:group_private_topic) do
|
||
Fabricate(:topic, category: private_category, user: user)
|
||
end
|
||
|
||
let(:recipient) { 'jake@adventuretime.ooo' }
|
||
|
||
it "should attach group to the invite" do
|
||
post "/t/#{group_private_topic.id}/invite.json", params: {
|
||
user: recipient,
|
||
group_ids: "#{group.id},123"
|
||
}
|
||
|
||
expect(response.status).to eq(200)
|
||
expect(Invite.find_by(email: recipient).groups).to eq([group])
|
||
end
|
||
|
||
describe 'when group is available to automatic groups only' do
|
||
before do
|
||
group.update!(automatic: true)
|
||
end
|
||
|
||
it 'should return the right response' do
|
||
post "/t/#{group_private_topic.id}/invite.json", params: {
|
||
user: user
|
||
}
|
||
|
||
expect(response.status).to eq(403)
|
||
end
|
||
end
|
||
|
||
describe 'when user is not part of the required group' do
|
||
it 'should return the right response' do
|
||
post "/t/#{group_private_topic.id}/invite.json", params: {
|
||
user: user
|
||
}
|
||
|
||
expect(response.status).to eq(422)
|
||
|
||
response_body = response.parsed_body
|
||
|
||
expect(response_body["errors"]).to eq([
|
||
I18n.t("topic_invite.failed_to_invite",
|
||
group_names: group.name
|
||
)
|
||
])
|
||
end
|
||
end
|
||
end
|
||
|
||
describe 'when topic id is invalid' do
|
||
it 'should return the right response' do
|
||
post "/t/999/invite.json", params: {
|
||
email: user.email
|
||
}
|
||
|
||
expect(response.status).to eq(400)
|
||
end
|
||
end
|
||
|
||
it 'requires an email parameter' do
|
||
post "/t/#{topic.id}/invite.json"
|
||
expect(response.status).to eq(400)
|
||
end
|
||
|
||
describe "when PM has reached maximum allowed numbers of recipients" do
|
||
fab!(:user2) { Fabricate(:user) }
|
||
let(:pm) { Fabricate(:private_message_topic, user: user) }
|
||
|
||
let(:moderator_pm) { Fabricate(:private_message_topic, user: moderator) }
|
||
|
||
before do
|
||
SiteSetting.max_allowed_message_recipients = 2
|
||
end
|
||
|
||
it "doesn't allow normal users to invite" do
|
||
post "/t/#{pm.id}/invite.json", params: {
|
||
user: user2.username
|
||
}
|
||
expect(response.status).to eq(422)
|
||
expect(response.parsed_body["errors"]).to contain_exactly(
|
||
I18n.t("pm_reached_recipients_limit", recipients_limit: SiteSetting.max_allowed_message_recipients)
|
||
)
|
||
end
|
||
|
||
it "allows staff to bypass limits" do
|
||
sign_in(moderator)
|
||
post "/t/#{moderator_pm.id}/invite.json", params: {
|
||
user: user2.username
|
||
}
|
||
expect(response.status).to eq(200)
|
||
expect(moderator_pm.reload.topic_allowed_users.count).to eq(3)
|
||
end
|
||
end
|
||
|
||
describe 'when user does not have permission to invite to the topic' do
|
||
let(:topic) { Fabricate(:private_message_topic) }
|
||
|
||
it "should return the right response" do
|
||
post "/t/#{topic.id}/invite.json", params: {
|
||
user: user.username
|
||
}
|
||
|
||
expect(response.status).to eq(403)
|
||
end
|
||
end
|
||
end
|
||
|
||
describe "when inviting a group to a topic" do
|
||
let(:group) { Fabricate(:group) }
|
||
|
||
before do
|
||
sign_in(admin)
|
||
end
|
||
|
||
it "should work correctly" do
|
||
email = 'hiro@from.heros'
|
||
|
||
post "/t/#{topic.id}/invite.json", params: {
|
||
email: email, group_ids: group.id
|
||
}
|
||
|
||
expect(response.status).to eq(200)
|
||
|
||
groups = Invite.find_by(email: email).groups
|
||
expect(groups.count).to eq(1)
|
||
expect(groups.first.id).to eq(group.id)
|
||
end
|
||
end
|
||
end
|
||
|
||
describe 'invite_group' do
|
||
let(:admins) { Group[:admins] }
|
||
let(:pm) { Fabricate(:private_message_topic) }
|
||
|
||
def invite_group(topic, expected_status)
|
||
post "/t/#{topic.id}/invite-group.json", params: { group: admins.name }
|
||
expect(response.status).to eq(expected_status)
|
||
end
|
||
|
||
before do
|
||
admins.update!(messageable_level: Group::ALIAS_LEVELS[:everyone])
|
||
end
|
||
|
||
describe 'as an anon user' do
|
||
it 'should be forbidden' do
|
||
invite_group(pm, 403)
|
||
end
|
||
end
|
||
|
||
describe 'as a normal user' do
|
||
let!(:user) { sign_in(Fabricate(:user)) }
|
||
|
||
describe 'when user does not have permission to view the topic' do
|
||
it 'should be forbidden' do
|
||
invite_group(pm, 403)
|
||
end
|
||
end
|
||
|
||
describe 'when user has permission to view the topic' do
|
||
before do
|
||
pm.allowed_users << user
|
||
end
|
||
|
||
it 'should allow user to invite group to topic' do
|
||
invite_group(pm, 200)
|
||
expect(pm.allowed_groups.first.id).to eq(admins.id)
|
||
end
|
||
end
|
||
end
|
||
|
||
describe 'as an admin user' do
|
||
before do
|
||
sign_in(admin)
|
||
end
|
||
|
||
it "disallows inviting a group to a topic" do
|
||
topic = Fabricate(:topic)
|
||
invite_group(topic, 422)
|
||
end
|
||
|
||
it "allows inviting a group to a PM" do
|
||
invite_group(pm, 200)
|
||
expect(pm.allowed_groups.first.id).to eq(admins.id)
|
||
end
|
||
end
|
||
|
||
context "when PM has reached maximum allowed numbers of recipients" do
|
||
let(:group) { Fabricate(:group, messageable_level: 99) }
|
||
let(:pm) { Fabricate(:private_message_topic, user: user) }
|
||
|
||
let(:moderator_pm) { Fabricate(:private_message_topic, user: moderator) }
|
||
|
||
before do
|
||
SiteSetting.max_allowed_message_recipients = 2
|
||
end
|
||
|
||
it "doesn't allow normal users to invite" do
|
||
post "/t/#{pm.id}/invite-group.json", params: {
|
||
group: group.name
|
||
}
|
||
expect(response.status).to eq(422)
|
||
expect(response.parsed_body["errors"]).to contain_exactly(
|
||
I18n.t("pm_reached_recipients_limit", recipients_limit: SiteSetting.max_allowed_message_recipients)
|
||
)
|
||
end
|
||
|
||
it "allows staff to bypass limits" do
|
||
sign_in(moderator)
|
||
post "/t/#{moderator_pm.id}/invite-group.json", params: {
|
||
group: group.name
|
||
}
|
||
expect(response.status).to eq(200)
|
||
expect(moderator_pm.reload.topic_allowed_users.count + moderator_pm.topic_allowed_groups.count).to eq(3)
|
||
end
|
||
end
|
||
end
|
||
|
||
describe 'shared drafts' do
|
||
let(:shared_drafts_category) { Fabricate(:category) }
|
||
|
||
before do
|
||
SiteSetting.shared_drafts_category = shared_drafts_category.id
|
||
end
|
||
|
||
describe "#update_shared_draft" do
|
||
let(:other_cat) { Fabricate(:category) }
|
||
let(:category) { Fabricate(:category) }
|
||
let(:topic) { Fabricate(:topic, category: shared_drafts_category, visible: false) }
|
||
|
||
context "anonymous" do
|
||
it "doesn't allow staff to update the shared draft" do
|
||
put "/t/#{topic.id}/shared-draft.json", params: { category_id: other_cat.id }
|
||
expect(response.code.to_i).to eq(403)
|
||
end
|
||
end
|
||
|
||
context "as a moderator" do
|
||
before do
|
||
sign_in(moderator)
|
||
end
|
||
|
||
context "with a shared draft" do
|
||
let!(:shared_draft) { Fabricate(:shared_draft, topic: topic, category: category) }
|
||
it "allows staff to update the category id" do
|
||
put "/t/#{topic.id}/shared-draft.json", params: { category_id: other_cat.id }
|
||
expect(response.status).to eq(200)
|
||
topic.reload
|
||
expect(topic.shared_draft.category_id).to eq(other_cat.id)
|
||
end
|
||
end
|
||
|
||
context "without a shared draft" do
|
||
it "allows staff to update the category id" do
|
||
put "/t/#{topic.id}/shared-draft.json", params: { category_id: other_cat.id }
|
||
expect(response.status).to eq(200)
|
||
topic.reload
|
||
expect(topic.shared_draft.category_id).to eq(other_cat.id)
|
||
end
|
||
end
|
||
end
|
||
end
|
||
|
||
describe "#publish" do
|
||
let(:category) { Fabricate(:category) }
|
||
let(:topic) { Fabricate(:topic, category: shared_drafts_category, visible: false) }
|
||
let!(:post) { Fabricate(:post, topic: topic) }
|
||
|
||
it "fails for anonymous users" do
|
||
put "/t/#{topic.id}/publish.json", params: { destination_category_id: category.id }
|
||
expect(response.status).to eq(403)
|
||
end
|
||
|
||
it "fails as a regular user" do
|
||
sign_in(user)
|
||
put "/t/#{topic.id}/publish.json", params: { destination_category_id: category.id }
|
||
expect(response.status).to eq(403)
|
||
end
|
||
|
||
context "as staff" do
|
||
before do
|
||
sign_in(moderator)
|
||
end
|
||
|
||
it "will publish the topic" do
|
||
put "/t/#{topic.id}/publish.json", params: { destination_category_id: category.id }
|
||
expect(response.status).to eq(200)
|
||
json = response.parsed_body['basic_topic']
|
||
|
||
result = Topic.find(json['id'])
|
||
expect(result.category_id).to eq(category.id)
|
||
expect(result.visible).to eq(true)
|
||
end
|
||
|
||
it 'fails if the destination category is the shared drafts category' do
|
||
put "/t/#{topic.id}/publish.json", params: { destination_category_id: shared_drafts_category.id }
|
||
expect(response.status).to eq(400)
|
||
end
|
||
end
|
||
end
|
||
end
|
||
|
||
describe "crawler" do
|
||
context "when not a crawler" do
|
||
it "renders with the application layout" do
|
||
get topic.url
|
||
|
||
body = response.body
|
||
|
||
expect(body).to have_tag(:script, with: { src: '/assets/application.js' })
|
||
expect(body).to have_tag(:meta, with: { name: 'fragment' })
|
||
end
|
||
end
|
||
|
||
context "when a crawler" do
|
||
it "renders with the crawler layout, and handles proper pagination" do
|
||
page1_time = 3.months.ago
|
||
page2_time = 2.months.ago
|
||
page3_time = 1.month.ago
|
||
|
||
freeze_time page1_time
|
||
|
||
topic = Fabricate(:topic)
|
||
Fabricate(:post, topic: topic)
|
||
Fabricate(:post, topic: topic)
|
||
|
||
freeze_time page2_time
|
||
Fabricate(:post, topic: topic)
|
||
Fabricate(:post, topic: topic)
|
||
|
||
freeze_time page3_time
|
||
Fabricate(:post, topic: topic)
|
||
|
||
# ugly, but no inteface to set this and we don't want to create
|
||
# 100 posts to test this thing
|
||
TopicView.stubs(:chunk_size).returns(2)
|
||
|
||
user_agent = "Mozilla/5.0 (compatible; Googlebot/2.1; +http://www.google.com/bot.html)"
|
||
|
||
get topic.url, env: { "HTTP_USER_AGENT" => user_agent }
|
||
|
||
body = response.body
|
||
|
||
expect(body).to have_tag(:body, with: { class: 'crawler' })
|
||
expect(body).to_not have_tag(:meta, with: { name: 'fragment' })
|
||
expect(body).to include('<link rel="next" href="' + topic.relative_url + "?page=2")
|
||
|
||
expect(response.headers['Last-Modified']).to eq(page1_time.httpdate)
|
||
|
||
get topic.url + "?page=2", env: { "HTTP_USER_AGENT" => user_agent }
|
||
body = response.body
|
||
|
||
expect(response.headers['Last-Modified']).to eq(page2_time.httpdate)
|
||
|
||
expect(body).to include('<link rel="prev" href="' + topic.relative_url)
|
||
expect(body).to include('<link rel="next" href="' + topic.relative_url + "?page=3")
|
||
|
||
get topic.url + "?page=3", env: { "HTTP_USER_AGENT" => user_agent }
|
||
body = response.body
|
||
|
||
expect(response.headers['Last-Modified']).to eq(page3_time.httpdate)
|
||
expect(body).to include('<link rel="prev" href="' + topic.relative_url + "?page=2")
|
||
end
|
||
|
||
context "canonical_url" do
|
||
fab!(:topic_embed) { Fabricate(:topic_embed, embed_url: "https://markvanlan.com") }
|
||
let(:user_agent) { "Mozilla/5.0 (compatible; Googlebot/2.1; +http://www.google.com/bot.html)" }
|
||
|
||
it "set to topic.url when embed_set_canonical_url is false" do
|
||
get topic_embed.topic.url, env: { "HTTP_USER_AGENT" => user_agent }
|
||
expect(response.body).to include('<link rel="canonical" href="' + topic_embed.topic.url)
|
||
end
|
||
|
||
it "set to topic_embed.embed_url when embed_set_canonical_url is true" do
|
||
SiteSetting.embed_set_canonical_url = true
|
||
get topic_embed.topic.url, env: { "HTTP_USER_AGENT" => user_agent }
|
||
expect(response.body).to include('<link rel="canonical" href="' + topic_embed.embed_url)
|
||
end
|
||
end
|
||
|
||
context "wayback machine" do
|
||
it "renders crawler layout" do
|
||
get topic.url, env: { "HTTP_USER_AGENT" => "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_4) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/74.0.3729.169 Safari/537.36", "HTTP_VIA" => "HTTP/1.0 web.archive.org (Wayback Save Page)" }
|
||
body = response.body
|
||
|
||
expect(body).to have_tag(:body, with: { class: 'crawler' })
|
||
expect(body).to_not have_tag(:meta, with: { name: 'fragment' })
|
||
end
|
||
end
|
||
end
|
||
end
|
||
|
||
describe "#reset_bump_date" do
|
||
context "errors" do
|
||
let(:topic) { Fabricate(:topic) }
|
||
|
||
it "needs you to be logged in" do
|
||
put "/t/#{topic.id}/reset-bump-date.json"
|
||
expect(response.status).to eq(403)
|
||
end
|
||
|
||
[:user].each do |user|
|
||
it "denies access for #{user}" do
|
||
sign_in(Fabricate(user))
|
||
put "/t/#{topic.id}/reset-bump-date.json"
|
||
expect(response.status).to eq(403)
|
||
end
|
||
end
|
||
|
||
it "should fail for non-existend topic" do
|
||
max_id = Topic.maximum(:id)
|
||
sign_in(admin)
|
||
put "/t/#{max_id + 1}/reset-bump-date.json"
|
||
expect(response.status).to eq(404)
|
||
end
|
||
end
|
||
|
||
[:admin, :moderator, :trust_level_4].each do |user|
|
||
it "should reset bumped_at as #{user}" do
|
||
sign_in(Fabricate(user))
|
||
topic = Fabricate(:topic, bumped_at: 1.hour.ago)
|
||
timestamp = 1.day.ago
|
||
Fabricate(:post, topic: topic, created_at: timestamp)
|
||
|
||
put "/t/#{topic.id}/reset-bump-date.json"
|
||
expect(response.status).to eq(200)
|
||
expect(topic.reload.bumped_at).to eq_time(timestamp)
|
||
end
|
||
end
|
||
end
|
||
end
|