mirror of
https://github.com/discourse/discourse.git
synced 2024-12-29 18:33:43 +08:00
5b19e2ca0f
The hierarchical search for categories is composed of several complex nested queries. This change ensures that the secured categories are filtered out as soon as possible to ensure that the default limit of 5 categories is reached. Without this fix, the search can return less than 5 categories if any of the first 5 categories cannot be displayed due to permissions.
1603 lines
55 KiB
Ruby
1603 lines
55 KiB
Ruby
# frozen_string_literal: true
|
|
|
|
RSpec.describe CategoriesController do
|
|
let(:admin) { Fabricate(:admin) }
|
|
let!(:category) { Fabricate(:category, user: admin) }
|
|
fab!(:user)
|
|
|
|
describe "#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"]).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).to have_http_status(:found)
|
|
expect(response).to redirect_to("/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).to have_http_status(:moved_permanently)
|
|
expect(response).to redirect_to(%r{/c/#{category.slug}})
|
|
end
|
|
|
|
it "returns the right response for a normal user" do
|
|
sign_in(user)
|
|
|
|
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,
|
|
)
|
|
end
|
|
|
|
it "does not returns subcategories without permission" do
|
|
subcategory = Fabricate(:category, user: admin, parent_category: category)
|
|
subcategory.set_permissions(admins: :full)
|
|
subcategory.save!
|
|
|
|
sign_in(user)
|
|
|
|
get "/categories.json?include_subcategories=true"
|
|
|
|
expect(response.status).to eq(200)
|
|
|
|
category_list = response.parsed_body["category_list"]
|
|
|
|
subcategories_for_category = category_list["categories"][1]["subcategory_list"]
|
|
expect(subcategories_for_category).to eq(nil)
|
|
end
|
|
|
|
it "returns the right subcategory response with permission" do
|
|
subcategory = Fabricate(:category, user: admin, parent_category: category)
|
|
|
|
sign_in(user)
|
|
|
|
get "/categories.json?include_subcategories=true"
|
|
|
|
expect(response.status).to eq(200)
|
|
|
|
category_list = response.parsed_body["category_list"]
|
|
|
|
subcategories_for_category = category_list["categories"][1]["subcategory_list"]
|
|
expect(subcategories_for_category.count).to eq(1)
|
|
expect(subcategories_for_category.first["parent_category_id"]).to eq(category.id)
|
|
expect(subcategories_for_category.first["id"]).to eq(subcategory.id)
|
|
end
|
|
|
|
it "does not return subcategories without query param" do
|
|
subcategory = Fabricate(:category, user: admin, parent_category: category)
|
|
|
|
sign_in(user)
|
|
|
|
get "/categories.json"
|
|
|
|
expect(response.status).to eq(200)
|
|
|
|
category_list = response.parsed_body["category_list"]
|
|
|
|
subcategories_for_category = category_list["categories"][1]["subcategory_list"]
|
|
expect(subcategories_for_category).to eq(nil)
|
|
end
|
|
|
|
it "includes topics for categories, subcategories and subsubcategories when requested" do
|
|
SiteSetting.max_category_nesting = 3
|
|
subcategory = Fabricate(:category, user: admin, parent_category: category)
|
|
subsubcategory = Fabricate(:category, user: admin, parent_category: subcategory)
|
|
|
|
topic1 = Fabricate(:topic, category: category)
|
|
topic2 = Fabricate(:topic, category: subcategory)
|
|
topic3 = Fabricate(:topic, category: subsubcategory)
|
|
CategoryFeaturedTopic.feature_topics
|
|
|
|
get "/categories.json?include_subcategories=true&include_topics=true"
|
|
expect(response.status).to eq(200)
|
|
|
|
category_list = response.parsed_body["category_list"]
|
|
|
|
category_response = category_list["categories"].find { |c| c["id"] == category.id }
|
|
expect(category_response["topics"].map { |c| c["id"] }).to contain_exactly(
|
|
topic1.id,
|
|
topic2.id,
|
|
topic3.id,
|
|
)
|
|
|
|
subcategory_response = category_response["subcategory_list"][0]
|
|
expect(subcategory_response["topics"].map { |c| c["id"] }).to contain_exactly(
|
|
topic2.id,
|
|
topic3.id,
|
|
)
|
|
|
|
subsubcategory_response = subcategory_response["subcategory_list"][0]
|
|
expect(subsubcategory_response["topics"].map { |c| c["id"] }).to contain_exactly(topic3.id)
|
|
end
|
|
|
|
describe "topics filtered by tag for categories when requested" do
|
|
fab!(:tag) { Fabricate(:tag, name: "test-tag") }
|
|
fab!(:tag_2) { Fabricate(:tag, name: "second-test-tag") }
|
|
let(:topics_with_filter_tag) { [] }
|
|
|
|
before { SiteSetting.max_category_nesting = 3 }
|
|
|
|
it "includes filtered topics for categories" do
|
|
2.times do |i|
|
|
topics_with_filter_tag << Fabricate(:topic, category: category, tags: [tag])
|
|
Fabricate(:topic, category: category, tags: [tag_2])
|
|
end
|
|
CategoryFeaturedTopic.feature_topics
|
|
|
|
get "/categories.json?tag=#{tag.name}&include_topics=true"
|
|
expect(response.status).to eq(200)
|
|
|
|
category_list = response.parsed_body["category_list"]
|
|
category_response = category_list["categories"].find { |c| c["id"] == category.id }
|
|
|
|
expect(category_response["topics"].map { |c| c["id"] }).to contain_exactly(
|
|
*topics_with_filter_tag.map(&:id),
|
|
)
|
|
end
|
|
|
|
it "includes filtered topics for subcategories" do
|
|
subcategory = Fabricate(:category, user: admin, parent_category: category)
|
|
|
|
2.times do |i|
|
|
topics_with_filter_tag << Fabricate(:topic, category: subcategory, tags: [tag])
|
|
Fabricate(:topic, category: subcategory, tags: [tag_2])
|
|
end
|
|
CategoryFeaturedTopic.feature_topics
|
|
|
|
get "/categories.json?tag=#{tag.name}&include_subcategories=true&include_topics=true"
|
|
expect(response.status).to eq(200)
|
|
|
|
category_list = response.parsed_body["category_list"]
|
|
category_response = category_list["categories"].find { |c| c["id"] == category.id }
|
|
subcategory_response = category_response["subcategory_list"][0]
|
|
|
|
expect(subcategory_response["topics"].map { |c| c["id"] }).to contain_exactly(
|
|
*topics_with_filter_tag.map(&:id),
|
|
)
|
|
end
|
|
|
|
it "includes filtered topics for subsubcategories" do
|
|
subcategory = Fabricate(:category, user: admin, parent_category: category)
|
|
subsubcategory = Fabricate(:category, user: admin, parent_category: subcategory)
|
|
|
|
2.times do |i|
|
|
topics_with_filter_tag << Fabricate(:topic, category: subsubcategory, tags: [tag])
|
|
Fabricate(:topic, category: subsubcategory, tags: [tag_2])
|
|
end
|
|
CategoryFeaturedTopic.feature_topics
|
|
|
|
get "/categories.json?tag=#{tag.name}&include_subcategories=true&include_topics=true"
|
|
expect(response.status).to eq(200)
|
|
|
|
category_list = response.parsed_body["category_list"]
|
|
category_response = category_list["categories"].find { |c| c["id"] == category.id }
|
|
subsubcategory_response = category_response["subcategory_list"][0]["subcategory_list"][0]
|
|
|
|
expect(subsubcategory_response["topics"].map { |c| c["id"] }).to contain_exactly(
|
|
*topics_with_filter_tag.map(&:id),
|
|
)
|
|
end
|
|
end
|
|
|
|
describe "categories and latest topics - ordered by created date" do
|
|
fab!(:category)
|
|
fab!(:topic1) do
|
|
Fabricate(
|
|
:topic,
|
|
category: category,
|
|
created_at: 5.days.ago,
|
|
updated_at: Time.now,
|
|
bumped_at: Time.now,
|
|
)
|
|
end
|
|
fab!(:topic2) do
|
|
Fabricate(:topic, category: category, created_at: 2.days.ago, bumped_at: 2.days.ago)
|
|
end
|
|
fab!(:topic3) do
|
|
Fabricate(:topic, category: category, created_at: 1.day.ago, bumped_at: 1.day.ago)
|
|
end
|
|
|
|
context "when order is not set to created date" do
|
|
before { SiteSetting.desktop_category_page_style = "categories_and_latest_topics" }
|
|
|
|
it "sorts topics by the default bump date" do
|
|
get "/categories_and_latest.json"
|
|
expect(response.status).to eq(200)
|
|
expect(response.parsed_body["topic_list"]["topics"].map { |t| t["id"] }).to eq(
|
|
[topic1.id, topic3.id, topic2.id],
|
|
)
|
|
end
|
|
|
|
it "does not include the sort parameter in more_topics_url" do
|
|
# we need to create more topics for more_topics_url to be serialized
|
|
SiteSetting.categories_topics = 5
|
|
Fabricate.times(
|
|
5,
|
|
:topic,
|
|
category: category,
|
|
created_at: 1.day.ago,
|
|
bumped_at: 1.day.ago,
|
|
)
|
|
|
|
get "/categories_and_latest.json"
|
|
expect(response.status).to eq(200)
|
|
expect(response.parsed_body["topic_list"]["more_topics_url"]).to start_with("/latest")
|
|
expect(response.parsed_body["topic_list"]["more_topics_url"]).not_to include("sort")
|
|
end
|
|
end
|
|
|
|
context "when order is set to created" do
|
|
before do
|
|
SiteSetting.desktop_category_page_style = "categories_and_latest_topics_created_date"
|
|
end
|
|
|
|
it "sorts topics by crated at date" do
|
|
get "/categories_and_latest.json"
|
|
expect(response.status).to eq(200)
|
|
expect(response.parsed_body["topic_list"]["topics"].map { |t| t["id"] }).to eq(
|
|
[topic3.id, topic2.id, topic1.id],
|
|
)
|
|
end
|
|
|
|
it "includes the sort parameter in more_topics_url" do
|
|
# we need to create more topics for more_topics_url to be serialized
|
|
SiteSetting.categories_topics = 5
|
|
Fabricate.times(
|
|
5,
|
|
:topic,
|
|
category: category,
|
|
created_at: 1.day.ago,
|
|
bumped_at: 1.day.ago,
|
|
)
|
|
|
|
get "/categories_and_latest.json"
|
|
expect(response.status).to eq(200)
|
|
expect(response.parsed_body["topic_list"]["more_topics_url"]).to start_with("/latest")
|
|
expect(response.parsed_body["topic_list"]["more_topics_url"]).to include("sort=created")
|
|
end
|
|
end
|
|
end
|
|
|
|
it "includes subcategories and topics by default when view is subcategories_with_featured_topics" do
|
|
SiteSetting.max_category_nesting = 3
|
|
subcategory = Fabricate(:category, user: admin, parent_category: category)
|
|
|
|
topic1 = Fabricate(:topic, category: category)
|
|
CategoryFeaturedTopic.feature_topics
|
|
|
|
SiteSetting.desktop_category_page_style = "subcategories_with_featured_topics"
|
|
get "/categories.json"
|
|
expect(response.status).to eq(200)
|
|
|
|
category_list = response.parsed_body["category_list"]
|
|
|
|
category_response = category_list["categories"].find { |c| c["id"] == category.id }
|
|
expect(category_response["topics"].map { |c| c["id"] }).to contain_exactly(topic1.id)
|
|
|
|
expect(category_response["subcategory_list"][0]["id"]).to eq(subcategory.id)
|
|
end
|
|
|
|
it "doesn't do more queries when more categories exist" do
|
|
SiteSetting.lazy_load_categories_groups = true
|
|
Theme.cache.clear
|
|
|
|
Fabricate(:category, parent_category: Fabricate(:category))
|
|
|
|
before_queries =
|
|
track_sql_queries do
|
|
get "/categories.json"
|
|
expect(response.status).to eq(200)
|
|
end
|
|
|
|
Fabricate(:category, parent_category: Fabricate(:category))
|
|
|
|
Theme.cache.clear
|
|
|
|
after_queries =
|
|
track_sql_queries do
|
|
get "/categories.json"
|
|
expect(response.status).to eq(200)
|
|
end
|
|
|
|
expect(after_queries.size).to eq(before_queries.size)
|
|
end
|
|
|
|
it "does not result in N+1 queries problem with multiple topics" do
|
|
category1 = Fabricate(:category)
|
|
category2 = Fabricate(:category)
|
|
upload = Fabricate(:upload)
|
|
topic1 = Fabricate(:topic, category: category1)
|
|
topic2 = Fabricate(:topic, category: category1, image_upload: upload)
|
|
|
|
CategoryFeaturedTopic.feature_topics
|
|
SiteSetting.desktop_category_page_style = "categories_with_featured_topics"
|
|
|
|
# warmup
|
|
get "/categories.json"
|
|
expect(response.status).to eq(200)
|
|
|
|
first_request_queries =
|
|
track_sql_queries do
|
|
get "/categories.json"
|
|
expect(response.status).to eq(200)
|
|
end
|
|
|
|
category_response =
|
|
response.parsed_body["category_list"]["categories"].find { |c| c["id"] == category1.id }
|
|
expect(category_response["topics"].count).to eq(2)
|
|
|
|
upload = Fabricate(:upload)
|
|
topic3 = Fabricate(:topic, category: category2, image_upload: upload)
|
|
CategoryFeaturedTopic.feature_topics
|
|
|
|
second_request_queries =
|
|
track_sql_queries do
|
|
get "/categories.json"
|
|
expect(response.status).to eq(200)
|
|
end
|
|
|
|
category1_response =
|
|
response.parsed_body["category_list"]["categories"].find { |c| c["id"] == category1.id }
|
|
category2_response =
|
|
response.parsed_body["category_list"]["categories"].find { |c| c["id"] == category2.id }
|
|
expect(category1_response["topics"].size).to eq(2)
|
|
expect(category2_response["topics"].size).to eq(1)
|
|
|
|
expect(first_request_queries.count).to eq(second_request_queries.count)
|
|
end
|
|
|
|
it "does not show uncategorized unless allow_uncategorized_topics" do
|
|
SiteSetting.desktop_category_page_style = "categories_boxes_with_topics"
|
|
|
|
uncategorized = Category.find(SiteSetting.uncategorized_category_id)
|
|
Fabricate(:topic, category: uncategorized)
|
|
CategoryFeaturedTopic.feature_topics
|
|
|
|
SiteSetting.allow_uncategorized_topics = false
|
|
|
|
get "/categories.json"
|
|
expect(
|
|
response.parsed_body["category_list"]["categories"].map { |x| x["id"] },
|
|
).not_to include(uncategorized.id)
|
|
end
|
|
|
|
describe "with page" do
|
|
before { sign_in(admin) }
|
|
|
|
let!(:category2) { Fabricate(:category, user: admin) }
|
|
let!(:category3) { Fabricate(:category, user: admin) }
|
|
|
|
it "paginates results when lazy_load_categories is enabled" do
|
|
SiteSetting.lazy_load_categories_groups = "#{Group::AUTO_GROUPS[:everyone]}"
|
|
|
|
stub_const(CategoryList, "CATEGORIES_PER_PAGE", 2) { get "/categories.json?page=1" }
|
|
expect(response.status).to eq(200)
|
|
expect(response.parsed_body["category_list"]["categories"].count).to eq(2)
|
|
|
|
stub_const(CategoryList, "CATEGORIES_PER_PAGE", 2) { get "/categories.json?page=2" }
|
|
expect(response.status).to eq(200)
|
|
expect(response.parsed_body["category_list"]["categories"].count).to eq(2)
|
|
end
|
|
|
|
it "paginates results when there are many categories" do
|
|
stub_const(CategoryList, "MAX_UNOPTIMIZED_CATEGORIES", 2) do
|
|
stub_const(CategoryList, "CATEGORIES_PER_PAGE", 2) { get "/categories.json?page=1" }
|
|
expect(response.status).to eq(200)
|
|
expect(response.parsed_body["category_list"]["categories"].count).to eq(2)
|
|
|
|
stub_const(CategoryList, "CATEGORIES_PER_PAGE", 2) { get "/categories.json?page=2" }
|
|
expect(response.status).to eq(200)
|
|
expect(response.parsed_body["category_list"]["categories"].count).to eq(2)
|
|
end
|
|
end
|
|
|
|
it "does not paginate results by default" do
|
|
stub_const(CategoryList, "CATEGORIES_PER_PAGE", 2) { get "/categories.json?page=1" }
|
|
expect(response.status).to eq(200)
|
|
expect(response.parsed_body["category_list"]["categories"].count).to eq(4)
|
|
|
|
stub_const(CategoryList, "CATEGORIES_PER_PAGE", 2) { get "/categories.json?page=2" }
|
|
expect(response.status).to eq(200)
|
|
expect(response.parsed_body["category_list"]["categories"].count).to eq(0)
|
|
end
|
|
|
|
it "does not error out if page is a nested parameter" do
|
|
get "/categories.json?page[foo]=2"
|
|
expect(response.status).to eq(200)
|
|
end
|
|
end
|
|
end
|
|
|
|
describe "extensibility event" do
|
|
before { sign_in(admin) }
|
|
|
|
it "triggers a extensibility event" do
|
|
event =
|
|
DiscourseEvent
|
|
.track_events do
|
|
put "/categories/#{category.id}.json",
|
|
params: {
|
|
name: "hello",
|
|
color: "ff0",
|
|
text_color: "fff",
|
|
}
|
|
end
|
|
.last
|
|
|
|
expect(event[:event_name]).to eq(:category_updated)
|
|
expect(event[:params].first).to eq(category)
|
|
end
|
|
end
|
|
|
|
describe "#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],
|
|
moderating_group_ids: [group.id],
|
|
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["moderating_group_ids"]).to eq([group.id])
|
|
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(6) # 1 + 5 (bootstrap mode)
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
describe "#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, group_users: [])
|
|
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
|
|
|
|
describe "#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
|
|
|
|
describe "#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
|
|
|
|
describe "#update" do
|
|
fab!(:mod_group_1) { Fabricate(:group) }
|
|
fab!(:mod_group_2) { Fabricate(:group) }
|
|
fab!(:mod_group_3) { Fabricate(:group) }
|
|
|
|
before { Jobs.run_immediately! }
|
|
|
|
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 { sign_in(admin) }
|
|
|
|
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@example.com")
|
|
|
|
put "/categories/#{category.id}.json",
|
|
params: {
|
|
name: "Email",
|
|
email_in: "mail@example.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)
|
|
form_template_1 = Fabricate(:form_template)
|
|
form_template_2 = Fabricate(:form_template)
|
|
|
|
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",
|
|
"running" => %w[turtle salamander],
|
|
},
|
|
minimum_required_tags: "",
|
|
allow_global_tags: "true",
|
|
required_tag_groups: [{ name: tag_group.name, min_count: 2 }],
|
|
form_template_ids: [form_template_1.id, form_template_2.id],
|
|
}
|
|
|
|
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",
|
|
"running" => %w[turtle salamander],
|
|
)
|
|
expect(category.minimum_required_tags).to eq(0)
|
|
expect(category.allow_global_tags).to eq(true)
|
|
expect(category.category_required_tag_groups.count).to eq(1)
|
|
expect(category.category_required_tag_groups.first.tag_group.id).to eq(tag_group.id)
|
|
expect(category.category_required_tag_groups.first.min_count).to eq(2)
|
|
expect(category.form_template_ids).to eq([form_template_1.id, form_template_2.id])
|
|
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(7) # 2 + 5 (bootstrap mode)
|
|
end
|
|
|
|
it "updates per-category settings correctly" do
|
|
category.require_topic_approval = false
|
|
category.require_reply_approval = false
|
|
|
|
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,
|
|
category_setting_attributes: {
|
|
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!(
|
|
category_required_tag_groups: [
|
|
CategoryRequiredTagGroup.new(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,
|
|
required_tag_groups: [],
|
|
}
|
|
|
|
expect(response.status).to eq(200)
|
|
category.reload
|
|
expect(category.category_required_tag_groups).to be_empty
|
|
end
|
|
|
|
it "does not update other fields" do
|
|
SiteSetting.tagging_enabled = true
|
|
tag_group_1 = Fabricate(:tag_group)
|
|
tag_group_2 = Fabricate(:tag_group)
|
|
|
|
category.update!(
|
|
allowed_tags: %w[hello world],
|
|
allowed_tag_groups: [tag_group_1.name],
|
|
category_required_tag_groups: [CategoryRequiredTagGroup.new(tag_group: tag_group_2)],
|
|
custom_fields: {
|
|
field_1: "hello",
|
|
field_2: "hello",
|
|
},
|
|
)
|
|
|
|
put "/categories/#{category.id}.json"
|
|
expect(response.status).to eq(200)
|
|
category.reload
|
|
expect(category.tags.pluck(:name)).to contain_exactly("hello", "world")
|
|
expect(category.tag_groups.pluck(:name)).to contain_exactly(tag_group_1.name)
|
|
expect(category.category_required_tag_groups.first.tag_group).to eq(tag_group_2)
|
|
expect(category.custom_fields).to eq({ "field_1" => "hello", "field_2" => "hello" })
|
|
|
|
put "/categories/#{category.id}.json",
|
|
params: {
|
|
allowed_tags: [],
|
|
custom_fields: {
|
|
field_1: nil,
|
|
},
|
|
}
|
|
expect(response.status).to eq(200)
|
|
category.reload
|
|
expect(category.tags).to be_blank
|
|
expect(category.tag_groups.pluck(:name)).to contain_exactly(tag_group_1.name)
|
|
expect(category.category_required_tag_groups.first.tag_group).to eq(tag_group_2)
|
|
expect(category.custom_fields).to eq({ "field_2" => "hello" })
|
|
|
|
put "/categories/#{category.id}.json",
|
|
params: {
|
|
allowed_tags: [],
|
|
allowed_tag_groups: [],
|
|
required_tag_groups: [],
|
|
custom_fields: {
|
|
field_1: "hi",
|
|
field_2: nil,
|
|
},
|
|
}
|
|
expect(response.status).to eq(200)
|
|
category.reload
|
|
expect(category.tags).to be_blank
|
|
expect(category.tag_groups).to be_blank
|
|
expect(category.category_required_tag_groups).to eq([])
|
|
expect(category.custom_fields).to eq({ "field_1" => "hi" })
|
|
expect(category.form_template_ids.count).to eq(0)
|
|
end
|
|
|
|
it "doesn't set category moderation groups if the enable_category_group_moderation setting is false" do
|
|
SiteSetting.enable_category_group_moderation = false
|
|
|
|
put "/categories/#{category.id}.json", params: { moderating_group_ids: [mod_group_1.id] }
|
|
expect(response.status).to eq(200)
|
|
expect(category.reload.moderating_groups).to be_blank
|
|
end
|
|
|
|
it "sets category moderation groups if the enable_category_group_moderation setting is true" do
|
|
SiteSetting.enable_category_group_moderation = true
|
|
|
|
put "/categories/#{category.id}.json", params: { moderating_group_ids: [mod_group_1.id] }
|
|
expect(response.status).to eq(200)
|
|
expect(category.reload.moderating_groups).to contain_exactly(mod_group_1)
|
|
end
|
|
|
|
it "removes category moderation groups and adds groups according to the moderating_group_ids param" do
|
|
SiteSetting.enable_category_group_moderation = true
|
|
|
|
category.update!(moderating_group_ids: [mod_group_2.id])
|
|
expect(category.reload.moderating_groups).to contain_exactly(mod_group_2)
|
|
|
|
put "/categories/#{category.id}.json",
|
|
params: {
|
|
moderating_group_ids: [mod_group_1.id, mod_group_3.id],
|
|
}
|
|
expect(response.status).to eq(200)
|
|
expect(category.reload.moderating_groups).to contain_exactly(mod_group_1, mod_group_3)
|
|
end
|
|
|
|
it "can remove all category moderation groups" do
|
|
SiteSetting.enable_category_group_moderation = true
|
|
|
|
category.update!(moderating_group_ids: [mod_group_2.id, mod_group_1.id])
|
|
expect(category.reload.moderating_groups).to contain_exactly(mod_group_2, mod_group_1)
|
|
|
|
put "/categories/#{category.id}.json", params: { moderating_group_ids: [] }
|
|
expect(response.status).to eq(200)
|
|
expect(category.reload.moderating_groups).to be_blank
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
describe "#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 { sign_in(admin) }
|
|
|
|
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
|
|
|
|
describe "#categories_and_topics" do
|
|
before { 10.times.each { Fabricate(:topic) } }
|
|
|
|
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(topic_list["topics"].size).to eq(5)
|
|
|
|
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
|
|
|
|
it "does not show uncategorized unless allow_uncategorized_topics" do
|
|
uncategorized = Category.find(SiteSetting.uncategorized_category_id)
|
|
Fabricate(:topic, category: uncategorized)
|
|
CategoryFeaturedTopic.feature_topics
|
|
|
|
SiteSetting.allow_uncategorized_topics = false
|
|
|
|
get "/categories_and_latest.json"
|
|
expect(
|
|
response.parsed_body["category_list"]["categories"].map { |x| x["id"] },
|
|
).not_to include(uncategorized.id)
|
|
end
|
|
|
|
it "includes more_topics_url in the response to /categories_and_latest" do
|
|
SiteSetting.categories_topics = 5
|
|
|
|
get "/categories_and_latest.json"
|
|
expect(response.status).to eq(200)
|
|
expect(response.parsed_body["topic_list"]["more_topics_url"]).to start_with("/latest")
|
|
end
|
|
|
|
it "includes more_topics_url in the response to /categories_and_top" do
|
|
SiteSetting.categories_topics = 5
|
|
|
|
Fabricate.times(10, :topic, category: category, like_count: 1000, posts_count: 100)
|
|
TopTopic.refresh!
|
|
|
|
get "/categories_and_top.json"
|
|
expect(response.status).to eq(200)
|
|
expect(response.parsed_body["topic_list"]["more_topics_url"]).to start_with("/top")
|
|
end
|
|
|
|
it "includes more_topics_url in the response to /categories_and_hot" do
|
|
SiteSetting.categories_topics = 5
|
|
|
|
Fabricate.times(10, :topic, category: category, like_count: 1000, posts_count: 100)
|
|
TopicHotScore.update_scores
|
|
|
|
get "/categories_and_hot.json"
|
|
expect(response.status).to eq(200)
|
|
expect(response.parsed_body["topic_list"]["more_topics_url"]).to start_with("/hot")
|
|
end
|
|
|
|
describe "Showing top topics from private categories" do
|
|
it "returns the top topic from the private category when the user is a member" do
|
|
restricted_group = Fabricate(:group)
|
|
private_cat = Fabricate(:private_category, group: restricted_group)
|
|
private_topic = Fabricate(:topic, category: private_cat, like_count: 1000, posts_count: 100)
|
|
TopTopic.refresh!
|
|
restricted_group.add(user)
|
|
sign_in(user)
|
|
|
|
get "/categories_and_top.json"
|
|
parsed_topic =
|
|
response
|
|
.parsed_body
|
|
.dig("topic_list", "topics")
|
|
.detect { |t| t.dig("id") == private_topic.id }
|
|
|
|
expect(parsed_topic).to be_present
|
|
end
|
|
end
|
|
|
|
describe "Showing hot topics from private categories" do
|
|
it "returns the hot topic from the private category when the user is a member" do
|
|
restricted_group = Fabricate(:group)
|
|
private_cat = Fabricate(:private_category, group: restricted_group)
|
|
private_topic = Fabricate(:topic, category: private_cat, like_count: 1000, posts_count: 100)
|
|
TopicHotScore.update_scores
|
|
restricted_group.add(user)
|
|
sign_in(user)
|
|
|
|
get "/categories_and_hot.json"
|
|
parsed_topic =
|
|
response
|
|
.parsed_body
|
|
.dig("topic_list", "topics")
|
|
.detect { |t| t.dig("id") == private_topic.id }
|
|
|
|
expect(parsed_topic).to be_present
|
|
end
|
|
end
|
|
end
|
|
|
|
describe "#visible_groups" do
|
|
fab!(:public_group) do
|
|
Fabricate(:group, visibility_level: Group.visibility_levels[:public], name: "aaa")
|
|
end
|
|
fab!(:private_group) do
|
|
Fabricate(:group, visibility_level: Group.visibility_levels[:staff], name: "bbb")
|
|
end
|
|
fab!(:user_only_group) do
|
|
Fabricate(:group, visibility_level: Group.visibility_levels[:members], name: "ccc")
|
|
end
|
|
|
|
it "responds with 404 when id param is invalid" do
|
|
get "/c/-9999/visible_groups.json"
|
|
|
|
expect(response.status).to eq(404)
|
|
end
|
|
|
|
it "responds with 403 when category is restricted to the current user" do
|
|
category.set_permissions(private_group.name => :full)
|
|
category.save!
|
|
|
|
get "/c/#{category.id}/visible_groups.json"
|
|
|
|
expect(response.status).to eq(403)
|
|
end
|
|
|
|
it "returns the names of the groups that are visible to an admin" do
|
|
sign_in(admin)
|
|
|
|
category.set_permissions(
|
|
private_group.name => :full,
|
|
public_group.name => :full,
|
|
user_only_group.name => :full,
|
|
)
|
|
|
|
category.save!
|
|
|
|
get "/c/#{category.id}/visible_groups.json"
|
|
|
|
expect(response.status).to eq(200)
|
|
expect(response.parsed_body["groups"]).to eq(
|
|
[public_group.name, private_group.name, user_only_group.name],
|
|
)
|
|
end
|
|
|
|
it "returns the names of the groups that are visible to a user and excludes the everyone group" do
|
|
private_group.add(user)
|
|
sign_in(user)
|
|
|
|
category.set_permissions(
|
|
private_group.name => :full,
|
|
public_group.name => :full,
|
|
user_only_group.name => :full,
|
|
)
|
|
|
|
category.save!
|
|
|
|
get "/c/#{category.id}/visible_groups.json"
|
|
|
|
expect(response.status).to eq(200)
|
|
expect(response.parsed_body["groups"]).to eq([public_group.name])
|
|
end
|
|
|
|
it "returns no groups if everyone can see it" do
|
|
sign_in(user)
|
|
|
|
category.set_permissions(
|
|
"everyone" => :readonly,
|
|
private_group.name => :full,
|
|
public_group.name => :full,
|
|
user_only_group.name => :full,
|
|
)
|
|
|
|
category.save!
|
|
|
|
get "/c/#{category.id}/visible_groups.json"
|
|
|
|
expect(response.status).to eq(200)
|
|
expect(response.parsed_body["groups"]).to eq([])
|
|
end
|
|
end
|
|
|
|
describe "#find" do
|
|
fab!(:group)
|
|
fab!(:category) { Fabricate(:category, name: "Foo") }
|
|
fab!(:subcategory) { Fabricate(:category, name: "Foobar", parent_category: category) }
|
|
|
|
context "with ids" do
|
|
it "returns the categories" do
|
|
get "/categories/find.json", params: { ids: [subcategory.id] }
|
|
|
|
expect(response.parsed_body["categories"].map { |c| c["id"] }).to eq([subcategory.id])
|
|
end
|
|
|
|
it "preloads user-specific fields" do
|
|
subcategory.update!(read_restricted: true)
|
|
|
|
get "/categories/find.json", params: { ids: [category.id] }
|
|
|
|
serialized = response.parsed_body["categories"].first
|
|
expect(serialized["notification_level"]).to eq(CategoryUser.default_notification_level)
|
|
expect(serialized["permission"]).to eq(nil)
|
|
expect(serialized["has_children"]).to eq(false)
|
|
expect(serialized["subcategory_count"]).to eq(nil)
|
|
end
|
|
|
|
it "does not return hidden category" do
|
|
category.update!(read_restricted: true)
|
|
|
|
get "/categories/find.json", params: { ids: [123_456_789] }
|
|
|
|
expect(response.status).to eq(404)
|
|
end
|
|
end
|
|
|
|
context "with slug path" do
|
|
it "returns the category" do
|
|
get "/categories/find.json",
|
|
params: {
|
|
slug_path_with_id: "#{category.slug}/#{category.id}",
|
|
}
|
|
|
|
expect(response.parsed_body["categories"].map { |c| c["id"] }).to eq([category.id])
|
|
end
|
|
|
|
it "returns the subcategory and ancestors" do
|
|
get "/categories/find.json",
|
|
params: {
|
|
slug_path_with_id: "#{subcategory.slug}/#{subcategory.id}",
|
|
}
|
|
|
|
expect(response.parsed_body["categories"].map { |c| c["id"] }).to eq(
|
|
[category.id, subcategory.id],
|
|
)
|
|
end
|
|
|
|
it "does not return hidden category" do
|
|
category.update!(read_restricted: true)
|
|
|
|
get "/categories/find.json",
|
|
params: {
|
|
slug_path_with_id: "#{category.slug}/#{category.id}",
|
|
}
|
|
|
|
expect(response.status).to eq(403)
|
|
end
|
|
end
|
|
|
|
it "returns user fields" do
|
|
sign_in(admin)
|
|
|
|
get "/categories/find.json", params: { slug_path_with_id: "#{category.slug}/#{category.id}" }
|
|
|
|
category = response.parsed_body["categories"].first
|
|
expect(category["notification_level"]).to eq(NotificationLevels.all[:regular])
|
|
expect(category["permission"]).to eq(CategoryGroup.permission_types[:full])
|
|
expect(category["has_children"]).to eq(true)
|
|
expect(category["subcategory_count"]).to eq(1)
|
|
end
|
|
|
|
context "with a read restricted child category" do
|
|
before_all { subcategory.update!(read_restricted: true) }
|
|
|
|
it "indicates to an admin that the category has a child" do
|
|
sign_in(admin)
|
|
|
|
get "/categories/find.json", params: { ids: [category.id] }
|
|
category = response.parsed_body["categories"].first
|
|
expect(category["has_children"]).to eq(true)
|
|
expect(category["subcategory_count"]).to eq(1)
|
|
end
|
|
|
|
it "indicates to a normal user that the category has no child" do
|
|
sign_in(user)
|
|
|
|
get "/categories/find.json", params: { ids: [category.id] }
|
|
category = response.parsed_body["categories"].first
|
|
expect(category["has_children"]).to eq(false)
|
|
expect(category["subcategory_count"]).to eq(nil)
|
|
end
|
|
end
|
|
end
|
|
|
|
describe "#search" do
|
|
fab!(:category) { Fabricate(:category, name: "Foo") }
|
|
fab!(:subcategory) { Fabricate(:category, name: "Foobar", parent_category: category) }
|
|
fab!(:category2) { Fabricate(:category, name: "Notfoo") }
|
|
|
|
before do
|
|
SearchIndexer.enable
|
|
[category, category2, subcategory].each { |c| SearchIndexer.index(c, force: true) }
|
|
end
|
|
|
|
it "does not generate N+1 queries" do
|
|
# Set up custom fields
|
|
Site.preloaded_category_custom_fields << "bob"
|
|
category2.upsert_custom_fields("bob" => "marley")
|
|
|
|
# Warm up caches
|
|
post "/categories/search.json", params: { term: "Notfoo" }
|
|
|
|
queries = track_sql_queries { post "/categories/search.json", params: { term: "Notfoo" } }
|
|
|
|
expect(queries.length).to eq(8)
|
|
|
|
expect(response.parsed_body["categories"].length).to eq(1)
|
|
expect(response.parsed_body["categories"][0]["custom_fields"]).to eq("bob" => "marley")
|
|
ensure
|
|
Site.reset_preloaded_category_custom_fields
|
|
end
|
|
|
|
context "without include_ancestors" do
|
|
it "doesn't return ancestors" do
|
|
post "/categories/search.json", params: { term: "Notfoo" }
|
|
|
|
expect(response.parsed_body).not_to have_key("ancestors")
|
|
end
|
|
end
|
|
|
|
context "with include_ancestors=false" do
|
|
it "returns ancestors" do
|
|
post "/categories/search.json", params: { term: "Notfoo", include_ancestors: false }
|
|
|
|
expect(response.parsed_body).not_to have_key("ancestors")
|
|
end
|
|
end
|
|
|
|
context "with include_ancestors=true" do
|
|
it "returns ancestors" do
|
|
post "/categories/search.json", params: { term: "Notfoo", include_ancestors: true }
|
|
|
|
expect(response.parsed_body).to have_key("ancestors")
|
|
end
|
|
end
|
|
|
|
context "with term" do
|
|
it "returns categories" do
|
|
post "/categories/search.json", params: { term: "Foo" }
|
|
|
|
expect(response.parsed_body["categories"].size).to eq(3)
|
|
expect(response.parsed_body["categories"].map { |c| c["name"] }).to contain_exactly(
|
|
"Foo",
|
|
"Foobar",
|
|
"Notfoo",
|
|
)
|
|
end
|
|
end
|
|
|
|
context "with parent_category_id" do
|
|
it "returns categories" do
|
|
post "/categories/search.json", params: { parent_category_id: category.id }
|
|
|
|
expect(response.parsed_body["categories"].size).to eq(1)
|
|
expect(response.parsed_body["categories"].map { |c| c["name"] }).to contain_exactly(
|
|
"Foobar",
|
|
)
|
|
end
|
|
|
|
it "can return only top-level categories" do
|
|
post "/categories/search.json", params: { parent_category_id: -1 }
|
|
|
|
expect(response.parsed_body["categories"].size).to eq(3)
|
|
expect(response.parsed_body["categories"].map { |c| c["name"] }).to contain_exactly(
|
|
"Uncategorized",
|
|
"Foo",
|
|
"Notfoo",
|
|
)
|
|
end
|
|
end
|
|
|
|
context "with include_uncategorized" do
|
|
it "returns Uncategorized" do
|
|
post "/categories/search.json", params: { include_uncategorized: true }
|
|
|
|
expect(response.parsed_body["categories"].size).to eq(4)
|
|
expect(response.parsed_body["categories"].map { |c| c["name"] }).to contain_exactly(
|
|
"Uncategorized",
|
|
"Foo",
|
|
"Foobar",
|
|
"Notfoo",
|
|
)
|
|
end
|
|
|
|
it "does not return Uncategorized" do
|
|
post "/categories/search.json", params: { include_uncategorized: false }
|
|
|
|
expect(response.parsed_body["categories"].size).to eq(3)
|
|
expect(response.parsed_body["categories"].map { |c| c["name"] }).to contain_exactly(
|
|
"Foo",
|
|
"Foobar",
|
|
"Notfoo",
|
|
)
|
|
end
|
|
end
|
|
|
|
context "with select_category_ids" do
|
|
it "returns categories" do
|
|
post "/categories/search.json", params: { select_category_ids: [category.id] }
|
|
|
|
expect(response.parsed_body["categories"].size).to eq(1)
|
|
expect(response.parsed_body["categories"].map { |c| c["name"] }).to contain_exactly("Foo")
|
|
end
|
|
|
|
it "works with empty categories list" do
|
|
post "/categories/search.json", params: { select_category_ids: [""] }
|
|
|
|
expect(response.parsed_body["categories"].size).to eq(0)
|
|
end
|
|
end
|
|
|
|
context "with reject_category_ids" do
|
|
it "returns categories" do
|
|
post "/categories/search.json", params: { reject_category_ids: [category2.id] }
|
|
|
|
expect(response.parsed_body["categories"].size).to eq(3)
|
|
expect(response.parsed_body["categories"].map { |c| c["name"] }).to contain_exactly(
|
|
"Uncategorized",
|
|
"Foo",
|
|
"Foobar",
|
|
)
|
|
end
|
|
|
|
it "works with empty categories list" do
|
|
post "/categories/search.json", params: { reject_category_ids: [""] }
|
|
|
|
expect(response.parsed_body["categories"].size).to eq(4)
|
|
expect(response.parsed_body["categories"].map { |c| c["name"] }).to contain_exactly(
|
|
"Uncategorized",
|
|
"Foo",
|
|
"Foobar",
|
|
"Notfoo",
|
|
)
|
|
end
|
|
end
|
|
|
|
context "with include_subcategories" do
|
|
it "returns categories" do
|
|
post "/categories/search.json", params: { include_subcategories: false }
|
|
|
|
expect(response.parsed_body["categories"].size).to eq(3)
|
|
expect(response.parsed_body["categories"].map { |c| c["name"] }).to contain_exactly(
|
|
"Uncategorized",
|
|
"Foo",
|
|
"Notfoo",
|
|
)
|
|
end
|
|
|
|
it "returns categories and subcategories" do
|
|
post "/categories/search.json", params: { include_subcategories: true }
|
|
|
|
expect(response.parsed_body["categories"].size).to eq(4)
|
|
expect(response.parsed_body["categories"].map { |c| c["name"] }).to contain_exactly(
|
|
"Uncategorized",
|
|
"Foo",
|
|
"Foobar",
|
|
"Notfoo",
|
|
)
|
|
end
|
|
end
|
|
|
|
context "with prioritized_category_id" do
|
|
it "returns categories" do
|
|
post "/categories/search.json", params: { prioritized_category_id: category2.id }
|
|
|
|
expect(response.parsed_body["categories"].size).to eq(4)
|
|
expect(response.parsed_body["categories"][0]["name"]).to eq("Notfoo")
|
|
end
|
|
end
|
|
|
|
context "with limit" do
|
|
it "returns categories" do
|
|
post "/categories/search.json", params: { limit: 2 }
|
|
|
|
expect(response.parsed_body["categories"].size).to eq(2)
|
|
end
|
|
end
|
|
|
|
context "with order" do
|
|
fab!(:category1) { Fabricate(:category, name: "Category Ordered", parent_category: category) }
|
|
fab!(:category2) { Fabricate(:category, name: "Ordered Category", parent_category: category) }
|
|
fab!(:category3) { Fabricate(:category, name: "Category Ordered") }
|
|
fab!(:category4) { Fabricate(:category, name: "Ordered Category") }
|
|
|
|
before do
|
|
[category1, category2, category3, category4].each do |c|
|
|
SearchIndexer.index(c, force: true)
|
|
end
|
|
end
|
|
|
|
it "returns in correct order" do
|
|
post "/categories/search.json", params: { term: "ordered" }
|
|
|
|
expect(response.parsed_body["categories"].map { |c| c["id"] }).to eq(
|
|
[category4.id, category2.id, category3.id, category1.id],
|
|
)
|
|
end
|
|
|
|
it "returns categories in the correct order when the limit is lower than the total number of categories" do
|
|
categories =
|
|
4.times.flat_map do |i|
|
|
post "/categories/search.json", params: { term: "ordered", page: i + 1, limit: 1 }
|
|
response.parsed_body["categories"]
|
|
end
|
|
|
|
expect(categories.map { |c| c["id"] }).to eq(
|
|
[category4.id, category2.id, category3.id, category1.id],
|
|
)
|
|
end
|
|
end
|
|
|
|
it "returns user fields" do
|
|
sign_in(admin)
|
|
|
|
post "/categories/search.json", params: { select_category_ids: [category.id] }
|
|
|
|
category = response.parsed_body["categories"].first
|
|
expect(category["notification_level"]).to eq(NotificationLevels.all[:regular])
|
|
expect(category["permission"]).to eq(CategoryGroup.permission_types[:full])
|
|
expect(category["has_children"]).to eq(true)
|
|
expect(category["subcategory_count"]).to eq(1)
|
|
end
|
|
|
|
it "doesn't expose secret categories" do
|
|
category.update!(read_restricted: true)
|
|
|
|
post "/categories/search.json", params: { term: "" }
|
|
|
|
expect(response.status).to eq(200)
|
|
expect(response.parsed_body["categories"].map { |c| c["id"] }).not_to include(category.id)
|
|
end
|
|
|
|
context "when not logged in" do
|
|
before { ActionController::Base.allow_forgery_protection = true }
|
|
after { ActionController::Base.allow_forgery_protection = false }
|
|
|
|
it "works and is not CSRF protected" do
|
|
post "/categories/search.json", params: { term: "" }
|
|
|
|
expect(response.status).to eq(200)
|
|
expect(response.parsed_body["categories"].map { |c| c["id"] }).to contain_exactly(
|
|
SiteSetting.uncategorized_category_id,
|
|
category.id,
|
|
subcategory.id,
|
|
category2.id,
|
|
)
|
|
end
|
|
end
|
|
end
|
|
|
|
describe "#hierachical_search" do
|
|
before { sign_in(user) }
|
|
|
|
it "produces categories with an empty term" do
|
|
get "/categories/hierarchical_search.json", params: { term: "" }
|
|
|
|
expect(response.status).to eq(200)
|
|
expect(response.parsed_body["categories"].length).not_to eq(0)
|
|
end
|
|
|
|
it "produces exactly 5 subcategories" do
|
|
subcategories = Fabricate.times(6, :category, parent_category: category)
|
|
subcategories[3].update!(read_restricted: true)
|
|
|
|
get "/categories/hierarchical_search.json"
|
|
|
|
expect(response.status).to eq(200)
|
|
expect(response.parsed_body["categories"].length).to eq(7)
|
|
expect(response.parsed_body["categories"].map { |c| c["id"] }).to contain_exactly(
|
|
category.id,
|
|
subcategories[0].id,
|
|
subcategories[1].id,
|
|
subcategories[2].id,
|
|
subcategories[4].id,
|
|
subcategories[5].id,
|
|
SiteSetting.uncategorized_category_id,
|
|
)
|
|
end
|
|
|
|
it "doesn't produce categories with a very specific term" do
|
|
get "/categories/hierarchical_search.json", params: { term: "acategorythatdoesnotexist" }
|
|
|
|
expect(response.status).to eq(200)
|
|
expect(response.parsed_body["categories"].length).to eq(0)
|
|
end
|
|
|
|
it "doesn't expose secret categories" do
|
|
category.update!(read_restricted: true)
|
|
|
|
get "/categories/hierarchical_search.json", params: { term: "" }
|
|
|
|
expect(response.status).to eq(200)
|
|
expect(response.parsed_body["categories"].map { |c| c["id"] }).not_to include(category.id)
|
|
end
|
|
end
|
|
end
|