discourse/spec/requests/categories_controller_spec.rb
Jarek Radosz cf44cdb082
FIX: Validate category name on parent change (#10815)
Previously, moving a category into another one, that already had a child category of that name (but with a non-conflicting slug) would cause a 500 error:

```
# PG::UniqueViolation:
#   ERROR:  duplicate key value violates unique constraint "unique_index_categories_on_name"
#   DETAIL:  Key (COALESCE(parent_category_id, '-1'::integer), name)=(5662, Amazing Category 0) already exists.
```

It now returns 422, and shows the same message as when you're renaming a category: "Category Name has already been taken".
2020-10-05 11:50:05 +02:00

540 lines
17 KiB
Ruby

# frozen_string_literal: true
require 'rails_helper'
describe CategoriesController do
let(:admin) { Fabricate(:admin) }
let!(:category) { Fabricate(:category, user: admin) }
fab!(:user) { Fabricate(:user) }
context 'index' do
it 'web crawler view has correct urls for subfolder install' do
set_subfolder "/forum"
get '/categories', headers: { 'HTTP_USER_AGENT' => 'Googlebot' }
html = Nokogiri::HTML5(response.body)
expect(html.css('body.crawler')).to be_present
expect(html.css("a[href=\"/forum/c/#{category.slug}/#{category.id}\"]")).to be_present
end
it "properly preloads topic list" do
SiteSetting.categories_topics = 5
SiteSetting.categories_topics.times { Fabricate(:topic) }
get "/categories"
expect(response.body).to have_tag("div#data-preloaded") do |element|
json = JSON.parse(element.current_scope.attribute('data-preloaded').value)
expect(json['topic_list_latest']).to include(%{"more_topics_url":"/latest"})
end
end
it "Shows correct title if category list is set for homepage" do
SiteSetting.top_menu = "categories|latest"
get "/"
expect(response.body).to have_tag "title", text: "Discourse"
SiteSetting.short_site_description = "Official community"
get "/"
expect(response.body).to have_tag "title", text: "Discourse - Official community"
end
it "redirects /category paths to /c paths" do
get "/category/uncategorized"
expect(response.status).to eq(302)
expect(response.body).to include("c/uncategorized")
end
it "respects permalinks before redirecting /category paths to /c paths" do
_perm = Permalink.create!(url: "category/something", category_id: category.id)
get "/category/something"
expect(response.status).to eq(301)
expect(response.body).to include(category.slug)
end
it 'returns the right response for a normal user' do
sign_in(user)
Draft.set(user, Draft::NEW_TOPIC, 0, 'hello')
get "/categories.json"
expect(response.status).to eq(200)
category_list = response.parsed_body["category_list"]
expect(category_list["categories"].map { |c| c["id"] }).to contain_exactly(
SiteSetting.get(:uncategorized_category_id), category.id
)
expect(category_list["draft_sequence"]).to eq(0)
expect(category_list["draft_key"]).to eq(Draft::NEW_TOPIC)
expect(category_list["draft"]).to eq('hello')
end
end
context 'extensibility event' do
before do
sign_in(admin)
end
it "triggers a extensibility event" do
event = DiscourseEvent.track_events {
put "/categories/#{category.id}.json", params: {
name: 'hello',
color: 'ff0',
text_color: 'fff'
}
}.last
expect(event[:event_name]).to eq(:category_updated)
expect(event[:params].first).to eq(category)
end
end
context '#create' do
it "requires the user to be logged in" do
post "/categories.json"
expect(response.status).to eq(403)
end
describe "logged in" do
before do
Jobs.run_immediately!
sign_in(admin)
end
it "raises an exception when they don't have permission to create it" do
sign_in(Fabricate(:user))
post "/categories.json", params: {
name: 'hello', color: 'ff0', text_color: 'fff'
}
expect(response).to be_forbidden
end
it "raises an exception when the name is missing" do
post "/categories.json", params: { color: "ff0", text_color: "fff" }
expect(response.status).to eq(400)
end
describe "failure" do
it "returns errors on a duplicate category name" do
category = Fabricate(:category, user: admin)
post "/categories.json", params: {
name: category.name, color: "ff0", text_color: "fff"
}
expect(response.status).to eq(422)
end
it "returns errors with invalid group" do
category = Fabricate(:category, user: admin)
readonly = CategoryGroup.permission_types[:readonly]
post "/categories.json", params: {
name: category.name, color: "ff0", text_color: "fff", permissions: { "invalid_group" => readonly }
}
expect(response.status).to eq(422)
expect(response.parsed_body['errors']).to be_present
end
end
describe "success" do
it "works" do
SiteSetting.enable_category_group_moderation = true
readonly = CategoryGroup.permission_types[:readonly]
create_post = CategoryGroup.permission_types[:create_post]
group = Fabricate(:group)
post "/categories.json", params: {
name: "hello",
color: "ff0",
text_color: "fff",
slug: "hello-cat",
auto_close_hours: 72,
search_priority: Searchable::PRIORITIES[:ignore],
reviewable_by_group_name: group.name,
permissions: {
"everyone" => readonly,
"staff" => create_post
}
}
expect(response.status).to eq(200)
cat_json = response.parsed_body['category']
expect(cat_json).to be_present
expect(cat_json['reviewable_by_group_name']).to eq(group.name)
expect(cat_json['name']).to eq('hello')
expect(cat_json['slug']).to eq('hello-cat')
expect(cat_json['color']).to eq('ff0')
expect(cat_json['auto_close_hours']).to eq(72)
expect(cat_json['search_priority']).to eq(Searchable::PRIORITIES[:ignore])
category = Category.find(cat_json['id'])
expect(category.category_groups.map { |g| [g.group_id, g.permission_type] }.sort).to eq([
[Group[:everyone].id, readonly], [Group[:staff].id, create_post]
])
expect(UserHistory.count).to eq(4) # 1 + 3 (bootstrap mode)
end
end
end
end
context '#show' do
before do
category.set_permissions(admins: :full)
category.save!
end
it "requires the user to be logged in" do
get "/c/#{category.id}/show.json"
expect(response.status).to eq(403)
end
describe "logged in" do
it "raises an exception if they don't have permission to see it" do
admin.update!(admin: false)
sign_in(admin)
get "/c/#{category.id}/show.json"
expect(response.status).to eq(403)
end
it "renders category for users that have permission" do
sign_in(admin)
get "/c/#{category.id}/show.json"
expect(response.status).to eq(200)
end
end
end
context '#destroy' do
it "requires the user to be logged in" do
delete "/categories/category.json"
expect(response.status).to eq(403)
end
describe "logged in" do
it "raises an exception if they don't have permission to delete it" do
admin.update!(admin: false)
sign_in(admin)
delete "/categories/#{category.slug}.json"
expect(response).to be_forbidden
end
it "deletes the record" do
sign_in(admin)
id = Fabricate(:topic_timer, category: category).id
expect do
delete "/categories/#{category.slug}.json"
end.to change(Category, :count).by(-1)
expect(response.status).to eq(200)
expect(UserHistory.count).to eq(1)
expect(TopicTimer.where(id: id).exists?).to eq(false)
end
end
end
context '#reorder' do
it "reorders the categories" do
sign_in(admin)
c1 = category
c2 = Fabricate(:category)
c3 = Fabricate(:category)
c4 = Fabricate(:category)
if c3.id < c2.id
tmp = c3; c2 = c3; c3 = tmp
end
c1.position = 8
c2.position = 6
c3.position = 7
c4.position = 5
payload = {}
payload[c1.id] = 4
payload[c2.id] = 6
payload[c3.id] = 6
payload[c4.id] = 5
post "/categories/reorder.json", params: { mapping: MultiJson.dump(payload) }
SiteSetting.fixed_category_positions = true
list = CategoryList.new(Guardian.new(admin))
expect(list.categories).to eq([
Category.find(SiteSetting.uncategorized_category_id),
c1,
c4,
c2,
c3
])
end
end
context '#update' do
before do
Jobs.run_immediately!
end
it "requires the user to be logged in" do
put "/categories/category.json"
expect(response.status).to eq(403)
end
describe "logged in" do
before do
sign_in(admin)
end
it "raises an exception if they don't have permission to edit it" do
sign_in(Fabricate(:user))
put "/categories/#{category.slug}.json", params: {
name: 'hello',
color: 'ff0',
text_color: 'fff'
}
expect(response).to be_forbidden
end
it "returns errors on a duplicate category name" do
other_category = Fabricate(:category, name: "Other", user: admin)
put "/categories/#{category.id}.json", params: {
name: other_category.name,
color: "ff0",
text_color: "fff",
}
expect(response.status).to eq(422)
end
it "returns errors when there is a name conflict while moving a category into another" do
parent_category = Fabricate(:category, name: "Parent", user: admin)
other_category = Fabricate(:category, name: category.name, user: admin, parent_category: parent_category, slug: "a-different-slug")
put "/categories/#{category.id}.json", params: {
parent_category_id: parent_category.id,
}
expect(response.status).to eq(422)
end
it "returns 422 if email_in address is already in use for other category" do
_other_category = Fabricate(:category, name: "Other", email_in: "mail@examle.com")
put "/categories/#{category.id}.json", params: {
name: "Email",
email_in: "mail@examle.com",
color: "ff0",
text_color: "fff",
}
expect(response.status).to eq(422)
end
describe "success" do
it "updates attributes correctly" do
SiteSetting.tagging_enabled = true
readonly = CategoryGroup.permission_types[:readonly]
create_post = CategoryGroup.permission_types[:create_post]
tag_group = Fabricate(:tag_group)
put "/categories/#{category.id}.json", params: {
name: "hello",
color: "ff0",
text_color: "fff",
slug: "hello-category",
auto_close_hours: 72,
permissions: {
"everyone" => readonly,
"staff" => create_post
},
custom_fields: {
"dancing" => "frogs"
},
minimum_required_tags: "",
allow_global_tags: 'true',
required_tag_group_name: tag_group.name,
min_tags_from_required_group: 2
}
expect(response.status).to eq(200)
category.reload
expect(category.category_groups.map { |g| [g.group_id, g.permission_type] }.sort).to eq([
[Group[:everyone].id, readonly], [Group[:staff].id, create_post]
])
expect(category.name).to eq("hello")
expect(category.slug).to eq("hello-category")
expect(category.color).to eq("ff0")
expect(category.auto_close_hours).to eq(72)
expect(category.custom_fields).to eq("dancing" => "frogs")
expect(category.minimum_required_tags).to eq(0)
expect(category.allow_global_tags).to eq(true)
expect(category.required_tag_group_id).to eq(tag_group.id)
expect(category.min_tags_from_required_group).to eq(2)
end
it 'logs the changes correctly' do
category.update!(permissions: { "admins" => CategoryGroup.permission_types[:create_post] })
put "/categories/#{category.id}.json", params: {
name: 'new name',
color: category.color,
text_color: category.text_color,
slug: category.slug,
permissions: {
"everyone" => CategoryGroup.permission_types[:create_post]
},
}
expect(response.status).to eq(200)
expect(UserHistory.count).to eq(5) # 2 + 3 (bootstrap mode)
end
it 'updates per-category settings correctly' do
category.custom_fields[Category::REQUIRE_TOPIC_APPROVAL] = false
category.custom_fields[Category::REQUIRE_REPLY_APPROVAL] = false
category.custom_fields[Category::NUM_AUTO_BUMP_DAILY] = 0
category.navigate_to_first_post_after_read = false
category.save!
put "/categories/#{category.id}.json", params: {
name: category.name,
color: category.color,
text_color: category.text_color,
navigate_to_first_post_after_read: true,
custom_fields: {
require_reply_approval: true,
require_topic_approval: true,
num_auto_bump_daily: 10
}
}
category.reload
expect(category.require_topic_approval?).to eq(true)
expect(category.require_reply_approval?).to eq(true)
expect(category.num_auto_bump_daily).to eq(10)
expect(category.navigate_to_first_post_after_read).to eq(true)
end
it "can remove required tag group" do
SiteSetting.tagging_enabled = true
category.update!(required_tag_group: Fabricate(:tag_group))
put "/categories/#{category.id}.json", params: {
name: category.name,
color: category.color,
text_color: category.text_color,
allow_global_tags: 'false',
min_tags_from_required_group: 1
}
expect(response.status).to eq(200)
category.reload
expect(category.required_tag_group).to be_nil
end
end
end
end
context '#update_slug' do
it 'requires the user to be logged in' do
put "/category/category/slug.json"
expect(response.status).to eq(403)
end
describe 'logged in' do
before do
sign_in(admin)
end
it 'rejects blank' do
put "/category/#{category.id}/slug.json", params: { slug: ' ' }
expect(response.status).to eq(422)
expect(response.parsed_body["errors"]).to eq(["Slug can't be blank"])
end
it 'accepts valid custom slug' do
put "/category/#{category.id}/slug.json", params: { slug: 'valid-slug' }
expect(response.status).to eq(200)
expect(category.reload.slug).to eq('valid-slug')
end
it 'accepts not well formed custom slug' do
put "/category/#{category.id}/slug.json", params: { slug: ' valid slug' }
expect(response.status).to eq(200)
expect(category.reload.slug).to eq('valid-slug')
end
it 'accepts and sanitize custom slug when the slug generation method is not ascii' do
SiteSetting.slug_generation_method = 'none'
put "/category/#{category.id}/slug.json", params: { slug: ' another !_ slug @' }
expect(response.status).to eq(200)
expect(category.reload.slug).to eq('another-slug')
SiteSetting.slug_generation_method = 'ascii'
end
it 'rejects invalid custom slug' do
put "/category/#{category.id}/slug.json", params: { slug: '.' }
expect(response.status).to eq(422)
expect(response.parsed_body["errors"]).to eq(["Slug is invalid"])
end
end
end
context '#categories_and_topics' do
before do
10.times.each { Fabricate(:topic) }
end
it 'works when SiteSetting.categories_topics is non-null' do
SiteSetting.categories_topics = 5
get '/categories_and_latest.json'
expect(response.parsed_body['topic_list']['topics'].size).to eq(5)
end
it 'works when SiteSetting.categories_topics is null' do
SiteSetting.categories_topics = 0
get '/categories_and_latest.json'
json = response.parsed_body
category_list = json['category_list']
topic_list = json['topic_list']
expect(category_list['categories'].size).to eq(2) # 'Uncategorized' and category
expect(category_list['draft_key']).to eq(Draft::NEW_TOPIC)
expect(category_list['draft_sequence']).to eq(nil)
expect(category_list['draft']).to eq(nil)
expect(topic_list['topics'].size).to eq(5)
expect(topic_list['draft_key']).to eq(Draft::NEW_TOPIC)
expect(topic_list['draft_sequence']).to eq(nil)
expect(topic_list['draft']).to eq(nil)
Fabricate(:category, parent_category: category)
get '/categories_and_latest.json'
json = response.parsed_body
expect(json['category_list']['categories'].size).to eq(2)
expect(json['topic_list']['topics'].size).to eq(5)
Fabricate(:category)
Fabricate(:category)
get '/categories_and_latest.json'
json = response.parsed_body
expect(json['category_list']['categories'].size).to eq(4)
expect(json['topic_list']['topics'].size).to eq(6)
end
end
end