discourse/spec/models/category_spec.rb
Régis Hanol 10f77556cd FIX: ensure no infinite category loop
If there's ever a circular reference in categories, don't go into an infinite loop when generating the category slug.

Instead, keep track of parent ids, and bail out as soon as we're encountering one more than once.
2024-05-06 18:02:22 +02:00

1554 lines
51 KiB
Ruby

# encoding: utf-8
# frozen_string_literal: true
RSpec.describe Category do
fab!(:user)
it_behaves_like "it has custom fields"
it { is_expected.to validate_presence_of :user_id }
it { is_expected.to validate_presence_of :name }
it do
is_expected.to validate_numericality_of(:default_slow_mode_seconds).is_greater_than(
0,
).only_integer
end
it "validates uniqueness of name" do
Fabricate(:category_with_definition)
is_expected.to validate_uniqueness_of(:name).scoped_to(:parent_category_id).case_insensitive
end
it "validates inclusion of search_priority" do
category = Fabricate.build(:category, user: user)
expect(category.valid?).to eq(true)
category.search_priority = Searchable::PRIORITIES.values.last + 1
expect(category.valid?).to eq(false)
expect(category.errors.to_hash.keys).to contain_exactly(:search_priority)
end
it "validates uniqueness in case insensitive way" do
Fabricate(:category_with_definition, name: "Cats")
cats = Fabricate.build(:category, name: "cats")
expect(cats).to_not be_valid
expect(cats.errors[:name]).to be_present
end
describe "Associations" do
it { is_expected.to have_one(:category_setting).dependent(:destroy) }
it "automatically creates a category setting" do
expect { Fabricate(:category) }.to change { CategorySetting.count }.by(1)
end
it "should delete associated sidebar_section_links when category is destroyed" do
category_sidebar_section_link = Fabricate(:category_sidebar_section_link)
Fabricate(:category_sidebar_section_link, linkable: category_sidebar_section_link.linkable)
tag_sidebar_section_link = Fabricate(:tag_sidebar_section_link)
expect { category_sidebar_section_link.linkable.destroy! }.to change {
SidebarSectionLink.count
}.from(12).to(10)
expect(SidebarSectionLink.last).to eq(tag_sidebar_section_link)
end
end
describe "slug" do
it "converts to lower" do
category = Category.create!(name: "Hello World", slug: "Hello-World", user: user)
expect(category.slug).to eq("hello-world")
end
end
describe "resolve_permissions" do
it "can determine read_restricted" do
read_restricted, resolved = Category.resolve_permissions(everyone: :full)
expect(read_restricted).to be false
expect(resolved).to be_blank
end
end
describe "permissions_params" do
it "returns the right group names and permission type" do
category = Fabricate(:category_with_definition)
group = Fabricate(:group)
category_group = Fabricate(:category_group, category: category, group: group)
expect(category.permissions_params).to eq("#{group.name}" => category_group.permission_type)
end
end
describe "#review_group_id" do
fab!(:group)
fab!(:category) { Fabricate(:category_with_definition, reviewable_by_group: group) }
fab!(:topic) { Fabricate(:topic, category: category) }
fab!(:post) { Fabricate(:post, topic: topic) }
fab!(:user) { Fabricate(:user, refresh_auto_groups: true) }
it "will add the group to the reviewable" do
SiteSetting.enable_category_group_moderation = true
reviewable = PostActionCreator.spam(user, post).reviewable
expect(reviewable.reviewable_by_group_id).to eq(group.id)
end
it "will add the group to the reviewable even if created manually" do
SiteSetting.enable_category_group_moderation = true
reviewable =
ReviewableFlaggedPost.create!(
created_by: user,
payload: {
raw: "test raw",
},
category: category,
)
expect(reviewable.reviewable_by_group_id).to eq(group.id)
end
it "will not add add the group to the reviewable" do
SiteSetting.enable_category_group_moderation = false
reviewable = PostActionCreator.spam(user, post).reviewable
expect(reviewable.reviewable_by_group_id).to be_nil
end
it "will nullify the group_id if destroyed" do
reviewable = PostActionCreator.spam(user, post).reviewable
group.destroy
expect(category.reload.reviewable_by_group).to be_blank
expect(reviewable.reload.reviewable_by_group_id).to be_blank
end
it "will remove the reviewable_by_group if the category is updated" do
SiteSetting.enable_category_group_moderation = true
reviewable = PostActionCreator.spam(user, post).reviewable
category.reviewable_by_group_id = nil
category.save!
expect(reviewable.reload.reviewable_by_group_id).to be_nil
end
end
describe "topic_create_allowed and post_create_allowed" do
fab!(:group)
fab!(:user) do
user = Fabricate(:user)
group.add(user)
group.save
user
end
fab!(:admin)
fab!(:default_category) { Fabricate(:category_with_definition) }
fab!(:full_category) do
c = Fabricate(:category_with_definition)
c.set_permissions(group => :full)
c.save
c
end
fab!(:can_post_category) do
c = Fabricate(:category_with_definition)
c.set_permissions(group => :create_post)
c.save
c
end
fab!(:can_read_category) do
c = Fabricate(:category_with_definition)
c.set_permissions(group => :readonly)
c.save
end
let(:user_guardian) { Guardian.new(user) }
let(:admin_guardian) { Guardian.new(admin) }
let(:anon_guardian) { Guardian.new(nil) }
context "when disabling uncategorized" do
before { SiteSetting.allow_uncategorized_topics = false }
it "allows everything to admins unconditionally" do
count = Category.count
expect(Category.topic_create_allowed(admin_guardian).count).to eq(count)
expect(Category.post_create_allowed(admin_guardian).count).to eq(count)
expect(Category.secured(admin_guardian).count).to eq(count)
end
it "allows normal users correct access to all categories" do
# Sam: I am mixed here, once disabling uncategorized maybe users should no
# longer be allowed to know about it so all counts should go down?
expect(Category.secured(user_guardian).count).to eq(5)
expect(Category.post_create_allowed(user_guardian).count).to eq(4)
expect(Category.topic_create_allowed(user_guardian).count).to eq(2)
end
end
it "allows everything to admins unconditionally" do
count = Category.count
expect(Category.topic_create_allowed(admin_guardian).count).to eq(count)
expect(Category.post_create_allowed(admin_guardian).count).to eq(count)
expect(Category.secured(admin_guardian).count).to eq(count)
end
it "allows normal users correct access to all categories" do
expect(Category.secured(user_guardian).count).to eq(5)
expect(Category.post_create_allowed(user_guardian).count).to eq(4)
expect(Category.topic_create_allowed(user_guardian).count).to eq(3)
end
it "allows anon correct access" do
expect(Category.scoped_to_permissions(anon_guardian, [:readonly]).count).to eq(2)
expect(Category.post_create_allowed(anon_guardian).count).to eq(0)
expect(Category.topic_create_allowed(anon_guardian).count).to eq(0)
# nil has special semantics
expect(Category.scoped_to_permissions(nil, [:readonly]).count).to eq(2)
end
it "handles :everyone scope" do
can_post_category.set_permissions(everyone: :create_post)
can_post_category.save
expect(Category.post_create_allowed(user_guardian).count).to eq(4)
# anonymous has permission to create no topics
expect(Category.scoped_to_permissions(user_guardian, [:readonly]).count).to eq(3)
end
end
describe "with_parents" do
fab!(:category)
fab!(:subcategory) { Fabricate(:category, parent_category: category) }
it "returns parent categories and subcategories" do
expect(Category.with_parents([category.id])).to contain_exactly(category)
end
it "returns only categories if top-level categories" do
expect(Category.with_parents([subcategory.id])).to contain_exactly(category, subcategory)
end
end
describe "security" do
fab!(:category) { Fabricate(:category_with_definition) }
fab!(:category_2) { Fabricate(:category_with_definition) }
fab!(:user)
fab!(:group)
it "secures categories correctly" do
expect(category.read_restricted?).to be false
category.set_permissions({})
expect(category.read_restricted?).to be true
category.set_permissions(everyone: :full)
expect(category.read_restricted?).to be false
expect(user.secure_categories).to be_empty
group.add(user)
group.save
category.set_permissions(group.id => :full)
category.save
user.reload
expect(user.secure_categories).to eq([category])
end
it "lists all secured categories correctly" do
uncategorized = Category.find(SiteSetting.uncategorized_category_id)
group.add(user)
category.set_permissions(group.id => :full)
category.save!
category_2.set_permissions(group.id => :full)
category_2.save!
expect(Category.secured).to match_array([uncategorized])
expect(Category.secured(Guardian.new(user))).to match_array(
[uncategorized, category, category_2],
)
end
end
it "strips leading blanks" do
expect(Fabricate(:category_with_definition, name: " music").name).to eq("music")
end
it "strips trailing blanks" do
expect(Fabricate(:category_with_definition, name: "bugs ").name).to eq("bugs")
end
it "strips leading and trailing blanks" do
expect(Fabricate(:category_with_definition, name: " blanks ").name).to eq("blanks")
end
it "sets name_lower" do
expect(Fabricate(:category_with_definition, name: "Not MySQL").name_lower).to eq("not mysql")
end
it "has custom fields" do
category = Fabricate(:category_with_definition, name: " music")
expect(category.custom_fields["a"]).to be_nil
category.custom_fields["bob"] = "marley"
category.custom_fields["jack"] = "black"
category.save
category = Category.find(category.id)
expect(category.custom_fields).to eq("bob" => "marley", "jack" => "black")
end
describe "short name" do
fab!(:category) { Fabricate(:category_with_definition, name: "xx") }
it "creates the category" do
expect(category).to be_present
end
it "has one topic" do
expect(Topic.where(category_id: category.id).count).to eq(1)
end
end
describe "non-english characters" do
context "when using ascii slug generator" do
before do
SiteSetting.slug_generation_method = "ascii"
@category = Fabricate(:category_with_definition, name: "测试")
end
after { @category.destroy }
it "creates a blank slug" do
expect(@category.slug).to be_blank
expect(@category.slug_for_url).to eq("#{@category.id}-category")
end
end
context "when using none slug generator" do
before do
SiteSetting.slug_generation_method = "none"
@category = Fabricate(:category_with_definition, name: "测试")
end
after do
SiteSetting.slug_generation_method = "ascii"
@category.destroy
end
it "creates a blank slug" do
expect(@category.slug).to be_blank
expect(@category.slug_for_url).to eq("#{@category.id}-category")
end
end
context "when using encoded slug generator" do
before do
SiteSetting.slug_generation_method = "encoded"
@category = Fabricate(:category_with_definition, name: "测试")
end
after do
SiteSetting.slug_generation_method = "ascii"
@category.destroy
end
it "creates a slug" do
expect(@category.slug).to eq("%E6%B5%8B%E8%AF%95")
expect(@category.slug_for_url).to eq("%E6%B5%8B%E8%AF%95")
end
it "keeps the encoded slug after saving" do
@category.save
expect(@category.slug).to eq("%E6%B5%8B%E8%AF%95")
expect(@category.slug_for_url).to eq("%E6%B5%8B%E8%AF%95")
end
end
end
describe "slug would be a number" do
let(:category) { Fabricate.build(:category, name: "2") }
it "creates a blank slug" do
expect(category.slug).to be_blank
expect(category.slug_for_url).to eq("#{category.id}-category")
end
end
describe "custom slug can be provided" do
it "can be sanitized" do
@c = Fabricate(:category_with_definition, name: "Fun Cats", slug: "fun-cats")
@cat = Fabricate(:category_with_definition, name: "love cats", slug: "love-cats")
@c.slug = " invalid slug"
@c.save
expect(@c.slug).to eq("invalid-slug")
c = Fabricate.build(:category, name: "More Fun Cats", slug: "love-cats")
expect(c).not_to be_valid
expect(c.errors[:slug]).to be_present
@cat.slug = "#{@c.id}-category"
expect(@cat).not_to be_valid
expect(@cat.errors[:slug]).to be_present
@cat.slug = "#{@cat.id}-category"
expect(@cat).to be_valid
expect(@cat.errors[:slug]).not_to be_present
end
context "if SiteSettings.slug_generation_method = ascii" do
before { SiteSetting.slug_generation_method = "ascii" }
it "fails if slug contains non-ascii characters" do
c = Fabricate.build(:category, name: "Sem acentuação", slug: "sem-acentuação")
expect(c).not_to be_valid
expect(c.errors[:slug]).to be_present
end
end
end
describe "description_text" do
it "correctly generates text description as needed" do
c = Category.new
expect(c.description_text).to be_nil
c.description = "&lt;hello <a>foo/bar</a>."
expect(c.description_text).to eq("&lt;hello foo/bar.")
end
end
describe "after create" do
before do
@category = Fabricate(:category_with_definition, name: "Amazing Category")
@topic = @category.topic
end
it "is created correctly" do
expect(@category.slug).to eq("amazing-category")
expect(@category.slug_for_url).to eq(@category.slug)
expect(@category.description).to be_blank
expect(Topic.where(category_id: @category).count).to eq(1)
expect(@topic).to be_present
expect(@topic.category).to eq(@category)
expect(@topic).to be_visible
expect(@topic.pinned_at).to be_present
expect(Guardian.new(@category.user).can_delete?(@topic)).to be false
expect(@topic.posts.count).to eq(1)
expect(@category.topic_url).to be_present
expect(@category.posts_week).to eq(0)
expect(@category.posts_month).to eq(0)
expect(@category.posts_year).to eq(0)
expect(@category.topics_week).to eq(0)
expect(@category.topics_month).to eq(0)
expect(@category.topics_year).to eq(0)
end
it "cooks the definition" do
category =
Category.create(
name: "little-test",
user_id: Discourse.system_user.id,
description: "click the link [here](https://fakeurl.com)",
)
expect(category.description.include?("[here]")).to eq(false)
expect(category.description).to eq(category.topic.first_post.cooked)
end
it "renames the definition when renamed" do
@category.update(name: "Troutfishing")
@topic.reload
expect(@topic.title).to match(/Troutfishing/)
expect(@topic.fancy_title).to match(/Troutfishing/)
end
it "doesn't raise an error if there is no definition topic to rename (uncategorized)" do
expect { @category.update(name: "Troutfishing", topic_id: nil) }.to_not raise_error
end
it "creates permalink when category slug is changed" do
@category.update(slug: "new-category")
expect(Permalink.count).to eq(1)
end
it "reuses existing permalink when category slug is changed" do
permalink = Permalink.create!(url: "c/#{@category.slug}/#{@category.id}", category_id: 42)
expect { @category.update(slug: "new-slug") }.to_not change { Permalink.count }
expect(permalink.reload.category_id).to eq(@category.id)
end
it "creates permalink when sub category slug is changed" do
sub_category =
Fabricate(:category_with_definition, slug: "sub-category", parent_category_id: @category.id)
sub_category.update(slug: "new-sub-category")
expect(Permalink.count).to eq(1)
end
it "deletes permalink when category slug is reused" do
Fabricate(:permalink, url: "/c/bikeshed-category")
Fabricate(:category_with_definition, slug: "bikeshed-category")
expect(Permalink.count).to eq(0)
end
it "deletes permalink when sub category slug is reused" do
Fabricate(:permalink, url: "/c/main-category/sub-category")
main_category = Fabricate(:category_with_definition, slug: "main-category")
Fabricate(
:category_with_definition,
slug: "sub-category",
parent_category_id: main_category.id,
)
expect(Permalink.count).to eq(0)
end
it "correctly creates permalink when category slug is changed in subfolder install" do
set_subfolder "/forum"
old_url = @category.url
@category.update(slug: "new-category")
permalink = Permalink.last
expect(permalink.url).to eq(old_url[1..-1])
end
it "should not set its description topic to auto-close" do
category = Fabricate(:category_with_definition, name: "Closing Topics", auto_close_hours: 1)
expect(category.topic.public_topic_timer).to eq(nil)
end
describe "creating a new category with the same slug" do
it "should have a blank slug if at the same level" do
category = Fabricate(:category_with_definition, name: "Amazing Categóry")
expect(category.slug).to be_blank
expect(category.slug_for_url).to eq("#{category.id}-category")
end
it "doesn't have a blank slug if not at the same level" do
parent = Fabricate(:category_with_definition, name: "Other parent")
category =
Fabricate(
:category_with_definition,
name: "Amazing Categóry",
parent_category_id: parent.id,
)
expect(category.slug).to eq("amazing-category")
expect(category.slug_for_url).to eq("amazing-category")
end
end
describe "trying to change the category topic's category" do
before do
@new_cat = Fabricate(:category_with_definition, name: "2nd Category", user: @category.user)
@topic.change_category_to_id(@new_cat.id)
@topic.reload
@category.reload
end
it "does not cause changes" do
expect(@category.topic_count).to eq(0)
expect(@topic.category).to eq(@category)
expect(@category.topic).to eq(@topic)
end
end
end
describe "new" do
subject(:category) { Fabricate.build(:category, user: Fabricate(:user)) }
it "triggers a extensibility event" do
event = DiscourseEvent.track_events { category.save! }.last
expect(event[:event_name]).to eq(:category_created)
expect(event[:params].first).to eq(category)
end
end
describe "update" do
it "should enforce uniqueness of slug" do
Fabricate(:category_with_definition, slug: "the-slug")
c2 = Fabricate(:category_with_definition, slug: "different-slug")
c2.slug = "the-slug"
expect(c2).to_not be_valid
expect(c2.errors[:slug]).to be_present
end
end
describe "destroy" do
before do
@category = Fabricate(:category_with_definition)
@category_id = @category.id
@topic_id = @category.topic_id
SiteSetting.shared_drafts_category = @category.id.to_s
end
it "is deleted correctly" do
@category.destroy
expect(Category.exists?(id: @category_id)).to be false
expect(Topic.with_deleted.where.not(deleted_at: nil).exists?(id: @topic_id)).to be true
expect(SiteSetting.shared_drafts_category).to be_blank
end
it "deletes related embeddable host" do
embeddable_host = Fabricate(:embeddable_host, category: @category)
@category.destroy!
expect { embeddable_host.reload }.to raise_error(ActiveRecord::RecordNotFound)
end
it "triggers a extensibility event" do
event = DiscourseEvent.track(:category_destroyed) { @category.destroy }
expect(event[:event_name]).to eq(:category_destroyed)
expect(event[:params].first).to eq(@category)
end
end
describe "latest" do
it "should be updated correctly" do
category = freeze_time(1.minute.ago) { Fabricate(:category_with_definition) }
post = create_post(category: category.id, created_at: 15.seconds.ago)
category.reload
expect(category.latest_post_id).to eq(post.id)
expect(category.latest_topic_id).to eq(post.topic_id)
post2 = create_post(category: category.id, created_at: 10.seconds.ago)
post3 = create_post(topic_id: post.topic_id, category: category.id, created_at: 5.seconds.ago)
category.reload
expect(category.latest_post_id).to eq(post3.id)
expect(category.latest_topic_id).to eq(post2.topic_id)
post3.reload
destroyer = PostDestroyer.new(Fabricate(:admin), post3)
destroyer.destroy
category.reload
expect(category.latest_post_id).to eq(post2.id)
end
end
describe "update_stats" do
before do
@category =
Fabricate(:category_with_definition, user: Fabricate(:user, refresh_auto_groups: true))
end
context "with regular topics" do
before do
create_post(user: @category.user, category: @category.id)
Category.update_stats
@category.reload
end
it "updates topic stats" do
expect(@category.topics_week).to eq(1)
expect(@category.topics_month).to eq(1)
expect(@category.topics_year).to eq(1)
expect(@category.topic_count).to eq(1)
expect(@category.post_count).to eq(1)
expect(@category.posts_year).to eq(1)
expect(@category.posts_month).to eq(1)
expect(@category.posts_week).to eq(1)
end
end
context "with deleted topics" do
before do
@category.topics << Fabricate(:deleted_topic, user: @category.user)
Category.update_stats
@category.reload
end
it "does not count deleted topics" do
expect(@category.topics_week).to eq(0)
expect(@category.topic_count).to eq(0)
expect(@category.topics_month).to eq(0)
expect(@category.topics_year).to eq(0)
expect(@category.post_count).to eq(0)
expect(@category.posts_year).to eq(0)
expect(@category.posts_month).to eq(0)
expect(@category.posts_week).to eq(0)
end
end
context "with revised post" do
before do
post = create_post(user: @category.user, category: @category.id)
SiteSetting.editing_grace_period = 1.minute
post.revise(post.user, { raw: "updated body" }, revised_at: post.updated_at + 2.minutes)
Category.update_stats
@category.reload
end
it "doesn't count each version of a post" do
expect(@category.post_count).to eq(1)
expect(@category.posts_year).to eq(1)
expect(@category.posts_month).to eq(1)
expect(@category.posts_week).to eq(1)
end
end
context "for uncategorized category" do
before do
@uncategorized = Category.find(SiteSetting.uncategorized_category_id)
create_post(user: Fabricate(:user, refresh_auto_groups: true), category: @uncategorized.id)
Category.update_stats
@uncategorized.reload
end
it "updates topic stats" do
expect(@uncategorized.topics_week).to eq(1)
expect(@uncategorized.topics_month).to eq(1)
expect(@uncategorized.topics_year).to eq(1)
expect(@uncategorized.topic_count).to eq(1)
expect(@uncategorized.post_count).to eq(1)
expect(@uncategorized.posts_year).to eq(1)
expect(@uncategorized.posts_month).to eq(1)
expect(@uncategorized.posts_week).to eq(1)
end
end
context "when there are no topics left" do
let!(:topic) { create_post(user: @category.user, category: @category.id).reload.topic }
it "can update the topic count to zero" do
@category.reload
expect(@category.topic_count).to eq(1)
expect(@category.topics.count).to eq(2)
topic.delete # Delete so the post trash/destroy hook doesn't fire
Category.update_stats
@category.reload
expect(@category.topics.count).to eq(1)
expect(@category.topic_count).to eq(0)
end
end
end
describe "#url" do
before_all { SiteSetting.max_category_nesting = 3 }
fab!(:category) { Fabricate(:category, name: "root") }
fab!(:sub_category) { Fabricate(:category, name: "child", parent_category_id: category.id) }
fab!(:sub_sub_category) do
Fabricate(:category, name: "child_of_child", parent_category_id: sub_category.id)
end
describe "for normal categories" do
it "builds a url" do
expect(category.url).to eq("/c/root/#{category.id}")
end
end
describe "for subcategories" do
it "builds a url" do
expect(sub_category.url).to eq("/c/root/child/#{sub_category.id}")
end
end
describe "for sub-sub-categories" do
it "builds a url" do
expect(sub_sub_category.url).to eq("/c/root/child/child-of-child/#{sub_sub_category.id}")
end
end
end
describe "uncategorized" do
let(:cat) { Category.where(id: SiteSetting.uncategorized_category_id).first }
it "reports as `uncategorized?`" do
expect(cat).to be_uncategorized
end
it "cannot have a parent category" do
cat.parent_category_id = Fabricate(:category_with_definition).id
expect(cat).to_not be_valid
end
end
describe "parent categories" do
fab!(:user)
fab!(:parent_category) { Fabricate(:category_with_definition, user: user) }
it "can be associated with a parent category" do
sub_category = Fabricate.build(:category, parent_category_id: parent_category.id, user: user)
expect(sub_category).to be_valid
expect(sub_category.parent_category).to eq(parent_category)
end
it "cannot associate a category with itself" do
category = Fabricate(:category_with_definition, user: user)
category.parent_category_id = category.id
expect(category).to_not be_valid
end
it "cannot have a category two levels deep" do
sub_category =
Fabricate(:category_with_definition, parent_category_id: parent_category.id, user: user)
nested_sub_category =
Fabricate.build(:category, parent_category_id: sub_category.id, user: user)
expect(nested_sub_category).to_not be_valid
end
describe ".query_parent_category" do
it "should return the parent category id given a parent slug" do
parent_category.name = "Amazing Category"
expect(parent_category.id).to eq(Category.query_parent_category(parent_category.slug))
end
end
describe ".query_category" do
it "should return the category" do
category =
Fabricate(
:category_with_definition,
name: "Amazing Category",
parent_category_id: parent_category.id,
user: user,
)
parent_category.name = "Amazing Parent Category"
expect(category).to eq(Category.query_category(category.slug, parent_category.id))
end
end
end
describe "find_by_email" do
it "is case insensitive" do
c1 = Fabricate(:category_with_definition, email_in: "lower@example.com")
c2 = Fabricate(:category_with_definition, email_in: "UPPER@EXAMPLE.COM")
c3 = Fabricate(:category_with_definition, email_in: "Mixed.Case@Example.COM")
expect(Category.find_by_email("LOWER@EXAMPLE.COM")).to eq(c1)
expect(Category.find_by_email("upper@example.com")).to eq(c2)
expect(Category.find_by_email("mixed.case@example.com")).to eq(c3)
expect(Category.find_by_email("MIXED.CASE@EXAMPLE.COM")).to eq(c3)
end
end
describe "find_by_slug" do
fab!(:category) { Fabricate(:category_with_definition, slug: "awesome-category") }
fab!(:subcategory) do
Fabricate(
:category_with_definition,
parent_category_id: category.id,
slug: "awesome-sub-category",
)
end
it "finds a category that exists" do
expect(Category.find_by_slug("awesome-category")).to eq(category)
end
it "finds a subcategory that exists" do
expect(Category.find_by_slug("awesome-sub-category", "awesome-category")).to eq(subcategory)
end
it "produces nil if the parent doesn't exist" do
expect(Category.find_by_slug("awesome-sub-category", "no-such-category")).to eq(nil)
end
it "produces nil if the parent doesn't exist and the requested category is a root category" do
expect(Category.find_by_slug("awesome-category", "no-such-category")).to eq(nil)
end
it "produces nil if the subcategory doesn't exist" do
expect(Category.find_by_slug("no-such-category", "awesome-category")).to eq(nil)
end
end
describe "validate email_in" do
fab!(:user)
it "works with a valid email" do
expect(Category.new(name: "test", user: user, email_in: "test@example.com").valid?).to eq(
true,
)
end
it "adds an error with an invalid email" do
category = Category.new(name: "test", user: user, email_in: "<sup>test</sup>")
expect(category.valid?).to eq(false)
expect(category.errors.full_messages.join).not_to match(/<sup>/)
end
context "with a duplicate email in a group" do
fab!(:group) { Fabricate(:group, name: "testgroup", incoming_email: "test@example.com") }
it "adds an error with an invalid email" do
category = Category.new(name: "test", user: user, email_in: group.incoming_email)
expect(category.valid?).to eq(false)
end
end
context "with duplicate email in a category" do
fab!(:category) do
Fabricate(
:category_with_definition,
user: user,
name: "<b>cool</b>",
email_in: "test@example.com",
)
end
it "adds an error with an invalid email" do
category = Category.new(name: "test", user: user, email_in: "test@example.com")
expect(category.valid?).to eq(false)
expect(category.errors.full_messages.join).not_to match(/<b>/)
end
end
end
describe "require topic/post approval" do
fab!(:category) { Fabricate(:category_with_definition) }
it "delegates methods to category settings" do
expect(category).to delegate_method(:require_reply_approval).to(:category_setting)
expect(category).to delegate_method(:require_reply_approval=).with_arguments(true).to(
:category_setting,
)
expect(category).to delegate_method(:require_reply_approval?).to(:category_setting)
expect(category).to delegate_method(:require_topic_approval).to(:category_setting)
expect(category).to delegate_method(:require_topic_approval=).with_arguments(true).to(
:category_setting,
)
expect(category).to delegate_method(:require_topic_approval?).to(:category_setting)
end
end
describe "auto bump" do
it "should correctly automatically bump topics" do
freeze_time
category = Fabricate(:category_with_definition, created_at: 1.minute.ago)
category.clear_auto_bump_cache!
post1 = create_post(category: category, created_at: 15.seconds.ago)
_post2 = create_post(category: category, created_at: 10.seconds.ago)
_post3 = create_post(category: category, created_at: 5.seconds.ago)
# no limits on post creation or category creation please
RateLimiter.enable
time = freeze_time 1.month.from_now
expect(category.auto_bump_topic!).to eq(false)
expect(Topic.where(bumped_at: time).count).to eq(0)
category.num_auto_bump_daily = 2
category.save!
expect(category.auto_bump_topic!).to eq(true)
expect(Topic.where(bumped_at: time).count).to eq(1)
# our extra bump message
expect(post1.topic.reload.posts_count).to eq(2)
time = freeze_time 13.hours.from_now
expect(category.auto_bump_topic!).to eq(true)
expect(Topic.where(bumped_at: time).count).to eq(1)
expect(category.auto_bump_topic!).to eq(false)
expect(Topic.where(bumped_at: time).count).to eq(1)
time = freeze_time 1.month.from_now
category.auto_bump_limiter.clear!
expect(Category.auto_bump_topic!).to eq(true)
expect(Topic.where(bumped_at: time).count).to eq(1)
category.num_auto_bump_daily = ""
category.save!
expect(Category.auto_bump_topic!).to eq(false)
end
it "should not auto-bump the same topic within the cooldown" do
freeze_time
category =
Fabricate(
:category_with_definition,
created_at: 1.minute.ago,
category_setting_attributes: {
auto_bump_cooldown_days: 1,
num_auto_bump_daily: 2,
},
)
category.clear_auto_bump_cache!
create_post(category: category, created_at: 15.seconds.ago)
# no limits on post creation or category creation please
RateLimiter.enable
time = freeze_time 1.month.from_now
expect(category.auto_bump_topic!).to eq(true)
expect(Topic.where(bumped_at: time).count).to eq(1)
time = freeze_time 13.hours.from_now
expect(category.auto_bump_topic!).to eq(false)
expect(Topic.where(bumped_at: time).count).to eq(0)
time = freeze_time 13.hours.from_now
expect(category.auto_bump_topic!).to eq(true)
expect(Topic.where(bumped_at: time).count).to eq(1)
end
it "should not automatically bump topics with a bump scheduled" do
freeze_time
category = Fabricate(:category_with_definition, created_at: 1.second.ago)
category.clear_auto_bump_cache!
post1 = create_post(category: category)
# no limits on post creation or category creation please
RateLimiter.enable
time = freeze_time 1.month.from_now
expect(category.auto_bump_topic!).to eq(false)
expect(Topic.where(bumped_at: time).count).to eq(0)
category.num_auto_bump_daily = 2
category.save!
topic = Topic.find_by_id(post1.topic_id)
TopicTimer.create!(
user_id: Discourse::SYSTEM_USER_ID,
topic: topic,
execute_at: 1.hour.from_now,
status_type: TopicTimer.types[:bump],
)
expect(
Topic.joins(:topic_timers).where(topic_timers: { status_type: 6, deleted_at: nil }).count,
).to eq(1)
expect(category.auto_bump_topic!).to eq(false)
expect(Topic.where(bumped_at: time).count).to eq(0)
# does not include a bump message
expect(post1.topic.reload.posts_count).to eq(1)
end
end
describe "validate permissions compatibility" do
fab!(:admin)
fab!(:group)
fab!(:group2) { Fabricate(:group) }
fab!(:parent_category) { Fabricate(:category_with_definition, name: "parent") }
fab!(:subcategory) do
Fabricate(:category_with_definition, name: "child1", parent_category_id: parent_category.id)
end
context "when changing subcategory permissions" do
it "it is not valid if permissions are less restrictive" do
subcategory.set_permissions(group => :readonly)
subcategory.save!
parent_category.set_permissions(group => :readonly)
parent_category.save!
subcategory.set_permissions(group => :full, group2 => :readonly)
expect(subcategory.valid?).to eq(false)
expect(subcategory.errors.full_messages).to contain_exactly(
I18n.t("category.errors.permission_conflict", group_names: group2.name),
)
end
it "is valid if permissions are same or more restrictive" do
subcategory.set_permissions(group => :full, group2 => :create_post)
subcategory.save!
parent_category.set_permissions(group => :full, group2 => :create_post)
parent_category.save!
subcategory.set_permissions(group => :create_post, group2 => :full)
expect(subcategory.valid?).to eq(true)
end
it "is valid if everyone has access to parent category" do
parent_category.set_permissions(everyone: :readonly)
parent_category.save!
subcategory.set_permissions(group => :create_post, group2 => :create_post)
expect(subcategory.valid?).to eq(true)
end
end
context "when changing parent category permissions" do
fab!(:subcategory2) do
Fabricate(:category_with_definition, name: "child2", parent_category_id: parent_category.id)
end
it "is not valid if subcategory permissions are less restrictive" do
subcategory.set_permissions(group => :create_post)
subcategory.save!
subcategory2.set_permissions(group => :create_post, group2 => :create_post)
subcategory2.save!
parent_category.set_permissions(group => :readonly)
expect(parent_category.valid?).to eq(false)
expect(parent_category.errors.full_messages).to contain_exactly(
I18n.t("category.errors.permission_conflict", group_names: group2.name),
)
end
it "is not valid if the subcategory has no category groups, but the parent does" do
parent_category.set_permissions(group => :readonly)
expect(parent_category).not_to be_valid
end
it "is valid if subcategory permissions are same or more restrictive" do
subcategory.set_permissions(group => :create_post)
subcategory.save!
subcategory2.set_permissions(group => :create_post, group2 => :create_post)
subcategory2.save!
parent_category.set_permissions(group => :full, group2 => :create_post)
expect(parent_category.valid?).to eq(true)
end
it "is valid if everyone has access to parent category" do
subcategory.set_permissions(group => :create_post)
subcategory.save
parent_category.set_permissions(everyone: :readonly)
expect(parent_category.valid?).to eq(true)
end
end
end
describe "tree metrics" do
fab!(:category) { Category.create!(user: user, name: "foo") }
fab!(:subcategory) { Category.create!(user: user, name: "bar", parent_category: category) }
context "with a self-parent" do
before_all { DB.exec(<<-SQL, id: category.id) }
UPDATE categories
SET parent_category_id = :id
WHERE id = :id
SQL
describe "#depth_of_descendants" do
it "should produce max_depth" do
expect(category.depth_of_descendants(3)).to eq(3)
end
end
describe "#height_of_ancestors" do
it "should produce max_height" do
expect(category.height_of_ancestors(3)).to eq(3)
end
end
end
context "with a prospective self-parent" do
before { category.parent_category_id = category.id }
describe "#depth_of_descendants" do
it "should produce max_depth" do
expect(category.depth_of_descendants(3)).to eq(3)
end
end
describe "#height_of_ancestors" do
it "should produce max_height" do
expect(category.height_of_ancestors(3)).to eq(3)
end
end
end
context "with a prospective loop" do
before { category.parent_category_id = subcategory.id }
describe "#depth_of_descendants" do
it "should produce max_depth" do
expect(category.depth_of_descendants(3)).to eq(3)
end
end
describe "#height_of_ancestors" do
it "should produce max_height" do
expect(category.height_of_ancestors(3)).to eq(3)
end
end
end
describe "#depth_of_descendants" do
it "should be 0 when the category has no descendants" do
expect(subcategory.depth_of_descendants).to eq(0)
end
it "should be 1 when the category has a descendant" do
expect(category.depth_of_descendants).to eq(1)
end
end
describe "#height_of_ancestors" do
it "should be 0 when the category has no ancestors" do
expect(category.height_of_ancestors).to eq(0)
end
it "should be 1 when the category has an ancestor" do
expect(subcategory.height_of_ancestors).to eq(1)
end
end
end
describe "messageBus" do
it "does not publish notification level when publishing to /categories" do
category = Fabricate(:category)
category.name = "Amazing category"
messages = MessageBus.track_publish("/categories") { category.save! }
expect(messages.length).to eq(1)
message = messages.first
category_hash = message.data[:categories].first
expect(category_hash[:name]).to eq(category.name)
expect(category_hash.key?(:notification_level)).to eq(false)
end
end
describe "#ensure_consistency!" do
it "creates category topic" do
# corrupt a category topic
uncategorized = Category.find(SiteSetting.uncategorized_category_id)
uncategorized.create_category_definition
uncategorized.topic.posts.first.destroy!
# make stuff extra broken
uncategorized.topic.trash!
category = Fabricate(:category_with_definition)
category_destroyed = Fabricate(:category_with_definition)
category_trashed = Fabricate(:category_with_definition)
category_topic_id = category.topic.id
category_destroyed.topic.destroy!
category_trashed.topic.trash!
Category.ensure_consistency!
# step one fix corruption
expect(uncategorized.reload.topic_id).to eq(nil)
Category.ensure_consistency!
# step two don't create a category definition for uncategorized
expect(uncategorized.reload.topic_id).to eq(nil)
expect(category.reload.topic_id).to eq(category_topic_id)
expect(category_destroyed.reload.topic).to_not eq(nil)
expect(category_trashed.reload.topic).to_not eq(nil)
end
end
describe "#find_by_slug_path" do
it "works for categories with slugs" do
category = Fabricate(:category, slug: "cat1")
expect(Category.find_by_slug_path(["cat1"])).to eq(category)
end
it "works for categories without slugs" do
SiteSetting.slug_generation_method = "none"
category = Fabricate(:category, slug: "cat1")
expect(Category.find_by_slug_path(["#{category.id}-category"])).to eq(category)
end
it "works for subcategories with slugs" do
category = Fabricate(:category, slug: "cat1")
subcategory = Fabricate(:category, slug: "cat2", parent_category: category)
expect(Category.find_by_slug_path(%w[cat1 cat2])).to eq(subcategory)
end
it "works for subcategories without slugs" do
SiteSetting.slug_generation_method = "none"
category = Fabricate(:category, slug: "cat1")
subcategory = Fabricate(:category, slug: "cat2", parent_category: category)
expect(Category.find_by_slug_path(["cat1", "#{subcategory.id}-category"])).to eq(subcategory)
expect(
Category.find_by_slug_path(["#{category.id}-category", "#{subcategory.id}-category"]),
).to eq(subcategory)
end
end
describe "#cannot_delete_reason" do
fab!(:admin)
let(:guardian) { Guardian.new(admin) }
fab!(:category)
describe "when category is uncategorized" do
it "should return the reason" do
category = Category.find(SiteSetting.uncategorized_category_id)
expect(category.cannot_delete_reason).to eq(I18n.t("category.cannot_delete.uncategorized"))
end
end
describe "when category has subcategories" do
it "should return the right reason" do
category.subcategories << Fabricate(:category)
expect(category.cannot_delete_reason).to eq(
I18n.t("category.cannot_delete.has_subcategories"),
)
end
end
describe "when category has topics" do
it "should return the right reason" do
topic =
Fabricate(
:topic,
title: "</a><script>alert(document.cookie);</script><a>",
category: category,
)
category.reload
expect(category.cannot_delete_reason).to eq(
I18n.t(
"category.cannot_delete.topic_exists",
count: 1,
topic_link:
"<a href=\"#{topic.url}\">&lt;/a&gt;&lt;script&gt;alert(document.cookie);&lt;/script&gt;&lt;a&gt;</a>",
),
)
end
end
end
describe "#deleting the general category" do
fab!(:category)
it "should empty out the general_category_id site_setting" do
SiteSetting.general_category_id = category.id
category.destroy
expect(SiteSetting.general_category_id).to_not eq(category.id)
expect(SiteSetting.general_category_id).to be < 1
end
end
describe ".ids_from_slugs" do
fab!(:category) { Fabricate(:category, slug: "category") }
fab!(:category2) { Fabricate(:category, slug: "category2") }
fab!(:subcategory) { Fabricate(:category, parent_category: category, slug: "subcategory") }
fab!(:subcategory2) { Fabricate(:category, parent_category: category2, slug: "subcategory") }
it "returns [] when inputs is []" do
expect(Category.ids_from_slugs([])).to eq([])
end
it 'returns the ids of category when input is ["category"]' do
expect(Category.ids_from_slugs(%w[category])).to contain_exactly(category.id)
end
it 'returns the ids of subcategory when input is ["category:subcategory"]' do
expect(Category.ids_from_slugs(%w[category:subcategory])).to contain_exactly(subcategory.id)
end
it 'returns the ids of subcategory2 when input is ["category2:subcategory"]' do
expect(Category.ids_from_slugs(%w[category2:subcategory])).to contain_exactly(subcategory2.id)
end
it "returns the ids of category and category2 when input is ['category', 'category2']" do
expect(Category.ids_from_slugs(%w[category category2])).to contain_exactly(
category.id,
category2.id,
)
end
it "returns the ids of subcategory and subcategory2 when input is ['category:subcategory', 'category2:subcategory']" do
expect(
Category.ids_from_slugs(%w[category:subcategory category2:subcategory]),
).to contain_exactly(subcategory.id, subcategory2.id)
end
it "returns the ids of subcategory when input is ['category:subcategory', 'invalid:subcategory']" do
expect(
Category.ids_from_slugs(%w[category:subcategory invalid:subcategory]),
).to contain_exactly(subcategory.id)
end
it 'returns the ids of sub-subcategory when input is ["category:subcategory:sub-subcategory"] and maximum category nesting is 3' do
SiteSetting.max_category_nesting = 3
sub_subcategory = Fabricate(:category, parent_category: subcategory, slug: "sub-subcategory")
expect(Category.ids_from_slugs(%w[category:subcategory:sub-subcategory])).to contain_exactly(
sub_subcategory.id,
)
end
it 'returns nil when input is ["category:invalid-slug:sub-subcategory"] and maximum category nesting is 3' do
SiteSetting.max_category_nesting = 3
Fabricate(:category, parent_category: subcategory, slug: "sub-subcategory")
expect(Category.ids_from_slugs(%w[category:invalid-slug:sub-subcategory])).to eq([])
end
it 'returns the ids of subcategory when input is ["category:subcategory:sub-subcategory"] but maximum category nesting is 2' do
SiteSetting.max_category_nesting = 2
expect(Category.ids_from_slugs(%w[category:subcategory:sub-subcategory])).to contain_exactly(
subcategory.id,
)
end
it 'returns the ids of subcategory and subcategory2 when input is ["subcategory"]' do
expect(Category.ids_from_slugs(%w[subcategory])).to contain_exactly(
subcategory.id,
subcategory2.id,
)
end
end
describe "allowed_tags=" do
let(:category) { Fabricate(:category) }
fab!(:tag)
fab!(:tag2) { Fabricate(:tag) }
before { SiteSetting.tagging_enabled = true }
it "can use existing tags for category tags" do
category.allowed_tags = [tag.name]
expect_same_tag_names(category.reload.tags, [tag])
end
context "with synonyms" do
fab!(:synonym) { Fabricate(:tag, name: "synonym", target_tag: tag) }
it "can use existing tags for category tags" do
category.allowed_tags = [tag.name, synonym.name]
category.reload
category.allowed_tags = [tag.name, synonym.name, tag2.name]
expect_same_tag_names(category.reload.tags, [tag.name, synonym.name, tag2.name])
end
end
end
describe "#slug_path" do
before { SiteSetting.max_category_nesting = 3 }
fab!(:grandparent) { Fabricate(:category, slug: "foo") }
fab!(:parent) { Fabricate(:category, parent_category: grandparent, slug: "bar") }
let(:child) { Fabricate(:category, parent_category: parent, slug: "boo") }
it "returns the slug for categories without parents" do
expect(grandparent.slug_path).to eq [grandparent.slug]
end
it "returns the slug for categories with parent" do
expect(parent.slug_path).to eq [grandparent.slug, parent.slug]
end
it "returns the slug for categories with grand-parent" do
expect(child.slug_path).to eq [grandparent.slug, parent.slug, child.slug]
end
it "avoids infinite loops with circular references" do
grandparent.parent_category = parent
grandparent.save!(validate: false)
expect(grandparent.slug_path).to eq [parent.slug, grandparent.slug]
expect(parent.slug_path).to eq [grandparent.slug, parent.slug]
end
end
describe "#slug_ref" do
fab!(:category) { Fabricate(:category, slug: "foo") }
it "returns the slug for categories without parents" do
expect(category.slug_ref).to eq("foo")
end
context "for category with parent" do
fab!(:subcategory) { Fabricate(:category, parent_category: category, slug: "bar") }
it "returns the parent and child slug ref with separator" do
expect(subcategory.slug_ref).to eq("foo#{Category::SLUG_REF_SEPARATOR}bar")
end
end
context "for category with multiple parents" do
let(:subcategory_1) { Fabricate(:category, parent_category: category, slug: "bar") }
let(:subcategory_2) { Fabricate(:category, parent_category: subcategory_1, slug: "boo") }
before { SiteSetting.max_category_nesting = 3 }
it "returns the parent and child slug ref with separator" do
expect(subcategory_2.slug_ref(depth: 2)).to eq(
"foo#{Category::SLUG_REF_SEPARATOR}bar#{Category::SLUG_REF_SEPARATOR}boo",
)
end
it "allows limiting depth" do
expect(subcategory_2.slug_ref(depth: 1)).to eq("bar#{Category::SLUG_REF_SEPARATOR}boo")
end
end
end
describe ".ancestors_of" do
fab!(:category)
fab!(:subcategory) { Fabricate(:category, parent_category: category) }
fab!(:sub_subcategory) do
SiteSetting.max_category_nesting = 3
Fabricate(:category, parent_category: subcategory)
end
it "finds the parent" do
expect(Category.ancestors_of([subcategory.id]).to_a).to eq([category])
end
it "finds the grandparent" do
expect(Category.ancestors_of([sub_subcategory.id]).to_a).to contain_exactly(
category,
subcategory,
)
end
it "respects the relation it's called on" do
expect(Category.where.not(id: category.id).ancestors_of([sub_subcategory.id]).to_a).to eq(
[subcategory],
)
end
end
end