diff --git a/app/models/concerns/category_hashtag.rb b/app/models/concerns/category_hashtag.rb index 52d4d01b5b5..135a903aee9 100644 --- a/app/models/concerns/category_hashtag.rb +++ b/app/models/concerns/category_hashtag.rb @@ -9,8 +9,8 @@ module CategoryHashtag # TODO (martin) Remove this when enable_experimental_hashtag_autocomplete # becomes the norm, it is reimplemented below for CategoryHashtagDataSourcee def query_from_hashtag_slug(category_slug) - slug_path = category_slug.split(SEPARATOR) - return nil if slug_path.empty? || slug_path.size > 2 + slug_path = split_slug_path(category_slug) + return if slug_path.blank? slug_path.map! { |slug| CGI.escape(slug) } if SiteSetting.slug_generation_method == "encoded" @@ -34,7 +34,9 @@ module CategoryHashtag category_slugs .map(&:downcase) .map do |slug| - slug_path = slug.split(":") + slug_path = split_slug_path(slug) + next if slug_path.blank? + if SiteSetting.slug_generation_method == "encoded" slug_path.map! { |slug| CGI.escape(slug) } end @@ -60,5 +62,11 @@ module CategoryHashtag end .compact end + + def split_slug_path(slug) + slug_path = slug.split(SEPARATOR) + return if slug_path.empty? || slug_path.size > 2 + slug_path + end end end diff --git a/spec/requests/hashtags_controller_spec.rb b/spec/requests/hashtags_controller_spec.rb index 2d2b0f3ca67..7fa05578838 100644 --- a/spec/requests/hashtags_controller_spec.rb +++ b/spec/requests/hashtags_controller_spec.rb @@ -1,11 +1,13 @@ # frozen_string_literal: true RSpec.describe HashtagsController do - fab!(:category) { Fabricate(:category) } - fab!(:tag) { Fabricate(:tag) } + fab!(:category) { Fabricate(:category, name: "Random", slug: "random") } + fab!(:tag) { Fabricate(:tag, name: "bug") } fab!(:group) { Fabricate(:group) } - fab!(:private_category) { Fabricate(:private_category, group: group) } + fab!(:private_category) do + Fabricate(:private_category, group: group, name: "Staff", slug: "staff") + end fab!(:hidden_tag) { Fabricate(:tag, name: "hidden") } let(:tag_group) do @@ -14,134 +16,397 @@ RSpec.describe HashtagsController do before do SiteSetting.tagging_enabled = true - - # TODO (martin) Remove when enable_experimental_hashtag_autocomplete is default for all sites - SiteSetting.enable_experimental_hashtag_autocomplete = false - tag_group end describe "#lookup" do - context "when logged in" do - context "as regular user" do - before { sign_in(Fabricate(:user)) } + context "when enable_experimental_hashtag_autocomplete disabled" do + # TODO (martin) Remove when enable_experimental_hashtag_autocomplete is default for all sites + before { SiteSetting.enable_experimental_hashtag_autocomplete = false } + context "when logged in" do + context "as regular user" do + before { sign_in(Fabricate(:user)) } - it "returns only valid categories and tags" do - get "/hashtags.json", - params: { - slugs: [category.slug, private_category.slug, "none", tag.name, hidden_tag.name], - } + it "returns only valid categories and tags" do + get "/hashtags.json", + params: { + slugs: [category.slug, private_category.slug, "none", tag.name, hidden_tag.name], + } - expect(response.status).to eq(200) - expect(response.parsed_body).to eq( - "categories" => { - category.slug => category.url, - }, - "tags" => { - tag.name => tag.full_url, - }, - ) + expect(response.status).to eq(200) + expect(response.parsed_body).to eq( + "categories" => { + category.slug => category.url, + }, + "tags" => { + tag.name => tag.full_url, + }, + ) + end + + it "handles tags with the TAG_HASHTAG_POSTFIX" do + get "/hashtags.json", + params: { + slugs: ["#{tag.name}#{PrettyText::Helpers::TAG_HASHTAG_POSTFIX}"], + } + + expect(response.status).to eq(200) + expect(response.parsed_body).to eq( + "categories" => { + }, + "tags" => { + tag.name => tag.full_url, + }, + ) + end + + it "does not return restricted categories or hidden tags" do + get "/hashtags.json", params: { slugs: [private_category.slug, hidden_tag.name] } + + expect(response.status).to eq(200) + expect(response.parsed_body).to eq("categories" => {}, "tags" => {}) + end end - it "handles tags with the TAG_HASHTAG_POSTFIX" do - get "/hashtags.json", - params: { - slugs: ["#{tag.name}#{PrettyText::Helpers::TAG_HASHTAG_POSTFIX}"], - } + context "as admin" do + fab!(:admin) { Fabricate(:admin) } - expect(response.status).to eq(200) - expect(response.parsed_body).to eq( - "categories" => { - }, - "tags" => { - tag.name => tag.full_url, - }, - ) + before { sign_in(admin) } + + it "returns restricted categories and hidden tags" do + group.add(admin) + + get "/hashtags.json", params: { slugs: [private_category.slug, hidden_tag.name] } + + expect(response.status).to eq(200) + expect(response.parsed_body).to eq( + "categories" => { + private_category.slug => private_category.url, + }, + "tags" => { + hidden_tag.name => hidden_tag.full_url, + }, + ) + end end - it "does not return restricted categories or hidden tags" do - get "/hashtags.json", params: { slugs: [private_category.slug, hidden_tag.name] } + context "with sub-sub-categories" do + before do + SiteSetting.max_category_nesting = 3 + sign_in(Fabricate(:user)) + end - expect(response.status).to eq(200) - expect(response.parsed_body).to eq("categories" => {}, "tags" => {}) + it "works" do + foo = Fabricate(:category_with_definition, slug: "foo") + foobar = Fabricate(:category_with_definition, slug: "bar", parent_category_id: foo.id) + foobarbaz = + Fabricate(:category_with_definition, slug: "baz", parent_category_id: foobar.id) + + qux = Fabricate(:category_with_definition, slug: "qux") + quxbar = Fabricate(:category_with_definition, slug: "bar", parent_category_id: qux.id) + quxbarbaz = + Fabricate(:category_with_definition, slug: "baz", parent_category_id: quxbar.id) + + invalid_slugs = [":"] + child_slugs = %w[bar baz] + deeply_nested_slugs = %w[foo:bar:baz qux:bar:baz] + get "/hashtags.json", + params: { + slugs: + invalid_slugs + child_slugs + deeply_nested_slugs + + %w[foo foo:bar bar:baz qux qux:bar], + } + + expect(response.status).to eq(200) + expect(response.parsed_body["categories"]).to eq( + "foo" => foo.url, + "foo:bar" => foobar.url, + "bar:baz" => foobarbaz.id < quxbarbaz.id ? foobarbaz.url : quxbarbaz.url, + "qux" => qux.url, + "qux:bar" => quxbar.url, + ) + end end end - context "as admin" do - fab!(:admin) { Fabricate(:admin) } - - before { sign_in(admin) } - - it "returns restricted categories and hidden tags" do - group.add(admin) - - get "/hashtags.json", params: { slugs: [private_category.slug, hidden_tag.name] } - - expect(response.status).to eq(200) - expect(response.parsed_body).to eq( - "categories" => { - private_category.slug => private_category.url, - }, - "tags" => { - hidden_tag.name => hidden_tag.full_url, - }, - ) + context "when not logged in" do + it "returns invalid access" do + get "/hashtags.json", params: { slugs: [] } + expect(response.status).to eq(403) end end + end - context "with sub-sub-categories" do - before do - SiteSetting.max_category_nesting = 3 - sign_in(Fabricate(:user)) - end + context "when enable_experimental_hashtag_autocomplete enabled" do + before { SiteSetting.enable_experimental_hashtag_autocomplete = true } - it "works" do - foo = Fabricate(:category_with_definition, slug: "foo") - foobar = Fabricate(:category_with_definition, slug: "bar", parent_category_id: foo.id) - foobarbaz = - Fabricate(:category_with_definition, slug: "baz", parent_category_id: foobar.id) + context "when logged in" do + context "as regular user" do + before { sign_in(Fabricate(:user)) } - qux = Fabricate(:category_with_definition, slug: "qux") - quxbar = Fabricate(:category_with_definition, slug: "bar", parent_category_id: qux.id) - quxbarbaz = - Fabricate(:category_with_definition, slug: "baz", parent_category_id: quxbar.id) + it "returns only valid categories and tags" do + get "/hashtags.json", + params: { + slugs: [category.slug, private_category.slug, "none", tag.name, hidden_tag.name], + order: %w[category tag], + } - get "/hashtags.json", - params: { - slugs: [ - ":", # should not work - "foo", - "bar", # should not work - "baz", # should not work - "foo:bar", - "bar:baz", - "foo:bar:baz", # should not work - "qux", - "qux:bar", - "qux:bar:baz", # should not work + expect(response.status).to eq(200) + expect(response.parsed_body).to eq( + { + "category" => [ + { + "relative_url" => category.url, + "text" => category.name, + "description" => nil, + "icon" => "folder", + "type" => "category", + "ref" => category.slug, + "slug" => category.slug, + }, ], - } + "tag" => [ + { + "relative_url" => tag.url, + "text" => tag.name, + "description" => nil, + "icon" => "tag", + "type" => "tag", + "ref" => tag.name, + "slug" => tag.name, + "secondary_text" => "x0", + }, + ], + }, + ) + end - expect(response.status).to eq(200) - expect(response.parsed_body["categories"]).to eq( - "foo" => foo.url, - "foo:bar" => foobar.url, - "bar:baz" => foobarbaz.id < quxbarbaz.id ? foobarbaz.url : quxbarbaz.url, - "qux" => qux.url, - "qux:bar" => quxbar.url, - ) + it "handles tags with the ::tag type suffix" do + get "/hashtags.json", params: { slugs: ["#{tag.name}::tag"], order: %w[category tag] } + + expect(response.status).to eq(200) + expect(response.parsed_body).to eq( + { + "category" => [], + "tag" => [ + { + "relative_url" => tag.url, + "text" => tag.name, + "description" => nil, + "icon" => "tag", + "type" => "tag", + "ref" => "#{tag.name}::tag", + "slug" => tag.name, + "secondary_text" => "x0", + }, + ], + }, + ) + end + + it "does not return restricted categories or hidden tags" do + get "/hashtags.json", + params: { + slugs: [private_category.slug, hidden_tag.name], + order: %w[category tag], + } + + expect(response.status).to eq(200) + expect(response.parsed_body).to eq({ "category" => [], "tag" => [] }) + end end + + context "as admin" do + fab!(:admin) { Fabricate(:admin) } + + before { sign_in(admin) } + + it "returns restricted categories and hidden tags" do + group.add(admin) + + get "/hashtags.json", + params: { + slugs: [private_category.slug, hidden_tag.name], + order: %w[category tag], + } + + expect(response.status).to eq(200) + expect(response.parsed_body).to eq( + { + "category" => [ + { + "relative_url" => private_category.url, + "text" => private_category.name, + "description" => nil, + "icon" => "folder", + "type" => "category", + "ref" => private_category.slug, + "slug" => private_category.slug, + }, + ], + "tag" => [ + { + "relative_url" => hidden_tag.url, + "text" => hidden_tag.name, + "description" => nil, + "icon" => "tag", + "type" => "tag", + "ref" => hidden_tag.name, + "slug" => hidden_tag.name, + "secondary_text" => "x0", + }, + ], + }, + ) + end + end + + context "with sub-sub-categories" do + before do + SiteSetting.max_category_nesting = 3 + sign_in(Fabricate(:user)) + end + + it "works" do + foo = Fabricate(:category_with_definition, slug: "foo") + foobar = Fabricate(:category_with_definition, slug: "bar", parent_category_id: foo.id) + foobarbaz = + Fabricate(:category_with_definition, slug: "baz", parent_category_id: foobar.id) + + qux = Fabricate(:category_with_definition, slug: "qux") + quxbar = Fabricate(:category_with_definition, slug: "bar", parent_category_id: qux.id) + quxbarbaz = + Fabricate(:category_with_definition, slug: "baz", parent_category_id: quxbar.id) + + invalid_slugs = [":"] + child_slugs = %w[bar baz] + deeply_nested_slugs = %w[foo:bar:baz qux:bar:baz] + get "/hashtags.json", + params: { + slugs: + invalid_slugs + child_slugs + deeply_nested_slugs + + %w[foo foo:bar bar:baz qux qux:bar], + order: %w[category tag], + } + + expect(response.status).to eq(200) + found_categories = response.parsed_body["category"] + expect(found_categories.map { |c| c["ref"] }).to match_array( + %w[foo foo:bar bar:baz qux qux:bar], + ) + expect(found_categories.find { |c| c["ref"] == "foo" }["relative_url"]).to eq(foo.url) + expect(found_categories.find { |c| c["ref"] == "foo:bar" }["relative_url"]).to eq( + foobar.url, + ) + expect(found_categories.find { |c| c["ref"] == "bar:baz" }["relative_url"]).to eq( + foobarbaz.id < quxbarbaz.id ? foobarbaz.url : quxbarbaz.url, + ) + expect(found_categories.find { |c| c["ref"] == "qux" }["relative_url"]).to eq(qux.url) + expect(found_categories.find { |c| c["ref"] == "qux:bar" }["relative_url"]).to eq( + quxbar.url, + ) + end + end + end + + context "when not logged in" do + it "returns invalid access" do + get "/hashtags.json", params: { slugs: [], order: %w[category tag] } + expect(response.status).to eq(403) + end + end + end + end + + describe "#search" do + fab!(:tag_2) { Fabricate(:tag, name: "random") } + + context "when logged in" do + before { sign_in(Fabricate(:user)) } + + it "returns the found category and then tag" do + get "/hashtags/search.json", params: { term: "rand", order: %w[category tag] } + expect(response.status).to eq(200) + expect(response.parsed_body["results"]).to eq( + [ + { + "relative_url" => category.url, + "text" => category.name, + "description" => nil, + "icon" => "folder", + "type" => "category", + "ref" => category.slug, + "slug" => category.slug, + }, + { + "relative_url" => tag_2.url, + "text" => tag_2.name, + "description" => nil, + "icon" => "tag", + "type" => "tag", + "ref" => "#{tag_2.name}::tag", + "slug" => tag_2.name, + "secondary_text" => "x0", + }, + ], + ) + end + + it "does not return hidden and restricted categories/tags" do + get "/hashtags/search.json", params: { term: "staff", order: %w[category tag] } + expect(response.status).to eq(200) + expect(response.parsed_body["results"]).to eq([]) + + get "/hashtags/search.json", params: { term: "hidden", order: %w[category tag] } + expect(response.status).to eq(200) + expect(response.parsed_body["results"]).to eq([]) + end + end + + context "when logged in as admin" do + before { sign_in(Fabricate(:admin)) } + + it "returns hidden and restricted categories/tags" do + get "/hashtags/search.json", params: { term: "staff", order: %w[category tag] } + expect(response.status).to eq(200) + expect(response.parsed_body["results"]).to eq( + [ + { + "relative_url" => private_category.url, + "text" => private_category.name, + "description" => nil, + "icon" => "folder", + "type" => "category", + "ref" => private_category.slug, + "slug" => private_category.slug, + }, + ], + ) + + get "/hashtags/search.json", params: { term: "hidden", order: %w[category tag] } + expect(response.status).to eq(200) + expect(response.parsed_body["results"]).to eq( + [ + { + "relative_url" => hidden_tag.url, + "text" => hidden_tag.name, + "description" => nil, + "icon" => "tag", + "type" => "tag", + "ref" => "#{hidden_tag.name}", + "slug" => hidden_tag.name, + "secondary_text" => "x0", + }, + ], + ) end end context "when not logged in" do it "returns invalid access" do - get "/hashtags.json", params: { slugs: [] } + get "/hashtags/search.json", params: { term: "rand", order: %w[category tag] } expect(response.status).to eq(403) end end end - - # TODO (martin) write a spec here for the new - # #lookup behaviour and the new #search behaviour end