mirror of
https://github.com/discourse/discourse.git
synced 2024-11-25 09:42:07 +08:00
19672faba6
Some checks are pending
Licenses / run (push) Waiting to run
Linting / run (push) Waiting to run
Tests / ${{ matrix.target }} ${{ matrix.build_type }} (system, plugins) (push) Waiting to run
Tests / ${{ matrix.target }} ${{ matrix.build_type }} (system, themes) (push) Waiting to run
Tests / ${{ matrix.target }} ${{ matrix.build_type }} (annotations, core) (push) Waiting to run
Tests / ${{ matrix.target }} ${{ matrix.build_type }} (backend, core) (push) Waiting to run
Tests / ${{ matrix.target }} ${{ matrix.build_type }} (backend, plugins) (push) Waiting to run
Tests / ${{ matrix.target }} ${{ matrix.build_type }} (frontend, plugins) (push) Waiting to run
Tests / ${{ matrix.target }} ${{ matrix.build_type }} (frontend, themes) (push) Waiting to run
Tests / ${{ matrix.target }} ${{ matrix.build_type }} (system, chat) (push) Waiting to run
Tests / ${{ matrix.target }} ${{ matrix.build_type }} (system, core) (push) Waiting to run
Tests / core frontend (${{ matrix.browser }}) (Chrome) (push) Waiting to run
Tests / core frontend (${{ matrix.browser }}) (Firefox ESR) (push) Waiting to run
Tests / core frontend (${{ matrix.browser }}) (Firefox Evergreen) (push) Waiting to run
This commit adds a new "Invite" link to the sidebar for all users who can invite to the site. Clicking the link opens the invite modal without changing the current route the user is on. Admins can customize the new link or remove it entirely if they wish by editing the sidebar section. Internal topic: t/129752.
1541 lines
50 KiB
Ruby
1541 lines
50 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)
|
|
category_sidebar_section_link_2 =
|
|
Fabricate(:category_sidebar_section_link, linkable: category_sidebar_section_link.linkable)
|
|
|
|
expect { category_sidebar_section_link.linkable.destroy! }.to change {
|
|
SidebarSectionLink.count
|
|
}.from(12).to(10)
|
|
expect(
|
|
SidebarSectionLink.where(
|
|
id: [category_sidebar_section_link.id, category_sidebar_section_link_2.id],
|
|
).count,
|
|
).to eq(0)
|
|
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 "#category_moderation_groups" do
|
|
fab!(:group)
|
|
fab!(:category) { Fabricate(:category_with_definition) }
|
|
fab!(:topic) { Fabricate(:topic, category: category) }
|
|
fab!(:post) { Fabricate(:post, topic: topic) }
|
|
fab!(:category_moderation_group) { Fabricate(:category_moderation_group, category:, group:) }
|
|
fab!(:user) { Fabricate(:user, refresh_auto_groups: true) }
|
|
|
|
it "is destroyed if the group is destroyed" do
|
|
expect(category.category_moderation_groups).to contain_exactly(category_moderation_group)
|
|
reviewable = PostActionCreator.spam(user, post).reviewable
|
|
group.destroy
|
|
expect(category.reload.category_moderation_groups).to be_blank
|
|
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 = "<hello <a>foo/bar</a>."
|
|
expect(c.description_text).to eq("<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}\"></a><script>alert(document.cookie);</script><a></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
|
|
|
|
describe ".limited_categories_matching" do
|
|
before_all { SiteSetting.max_category_nesting = 3 }
|
|
|
|
fab!(:foo) { Fabricate(:category, name: "foo") }
|
|
fab!(:bar) { Fabricate(:category, name: "bar", parent_category: foo) }
|
|
fab!(:baz) { Fabricate(:category, name: "baz", parent_category: bar) }
|
|
|
|
it "produces results in depth-first pre-order" do
|
|
SiteSetting.max_category_nesting = 3
|
|
expect(Category.limited_categories_matching(nil, nil, nil, "baz").pluck(:name)).to eq(
|
|
%w[foo bar baz],
|
|
)
|
|
end
|
|
end
|
|
end
|