diff --git a/app/assets/javascripts/discourse/models/tag-group.js.es6 b/app/assets/javascripts/discourse/models/tag-group.js.es6 index cfbd83e1640..f433038abbe 100644 --- a/app/assets/javascripts/discourse/models/tag-group.js.es6 +++ b/app/assets/javascripts/discourse/models/tag-group.js.es6 @@ -24,7 +24,8 @@ const TagGroup = RestModel.extend({ name: this.get('name'), tag_names: this.get('tag_names'), parent_tag_name: this.get('parent_tag_name') ? this.get('parent_tag_name') : undefined, - one_per_topic: this.get('one_per_topic') + one_per_topic: this.get('one_per_topic'), + permissions: this.get('visible_only_to_staff') ? {"staff": "1"} : {"everyone": "1"} }, type: isNew ? 'POST' : 'PUT' }).then(function(result) { diff --git a/app/assets/javascripts/discourse/templates/tag-groups-show.hbs b/app/assets/javascripts/discourse/templates/tag-groups-show.hbs index 0a7dc1ef704..032f0ce06eb 100644 --- a/app/assets/javascripts/discourse/templates/tag-groups-show.hbs +++ b/app/assets/javascripts/discourse/templates/tag-groups-show.hbs @@ -28,6 +28,13 @@ +
+ +
+ {{model.savingStatus}} diff --git a/app/controllers/tag_groups_controller.rb b/app/controllers/tag_groups_controller.rb index 8d819827f76..069f73830ab 100644 --- a/app/controllers/tag_groups_controller.rb +++ b/app/controllers/tag_groups_controller.rb @@ -70,7 +70,20 @@ class TagGroupsController < ApplicationController end def tag_groups_params - result = params.permit(:id, :name, :one_per_topic, tag_names: [], parent_tag_name: []) + if permissions = params[:permissions] + permissions.each do |k, v| + permissions[k] = v.to_i + end + end + + result = params.permit( + :id, + :name, + :one_per_topic, + tag_names: [], + parent_tag_name: [], + permissions: [*permissions&.keys] + ) result[:tag_names] ||= [] result[:parent_tag_name] ||= [] result[:one_per_topic] = (params[:one_per_topic] == "true") diff --git a/app/controllers/tags_controller.rb b/app/controllers/tags_controller.rb index 3a6d5f0d674..5c4e798b997 100644 --- a/app/controllers/tags_controller.rb +++ b/app/controllers/tags_controller.rb @@ -44,7 +44,10 @@ class TagsController < ::ApplicationController extras: { tag_groups: grouped_tag_counts } } else - unrestricted_tags = Tag.where("tags.id NOT IN (select tag_id from category_tags) AND tags.topic_count > 0") + unrestricted_tags = DiscourseTagging.filter_visible( + Tag.where("tags.id NOT IN (select tag_id from category_tags) AND tags.topic_count > 0"), + guardian + ) categories = Category.where("id in (select category_id from category_tags)") .where("id in (?)", guardian.allowed_category_ids) diff --git a/app/models/category.rb b/app/models/category.rb index e551227ed6f..b2b7242873d 100644 --- a/app/models/category.rb +++ b/app/models/category.rb @@ -321,6 +321,30 @@ SQL end end + def self.resolve_permissions(permissions) + read_restricted = true + + everyone = Group::AUTO_GROUPS[:everyone] + full = CategoryGroup.permission_types[:full] + + mapped = permissions.map do |group, permission| + group_id = Group.group_id_from_param(group) + permission = CategoryGroup.permission_types[permission] unless permission.is_a?(Integer) + + [group_id, permission] + end + + mapped.each do |group, permission| + if group == everyone && permission == full + return [false, []] + end + + read_restricted = false if group == everyone + end + + [read_restricted, mapped] + end + def allowed_tags=(tag_names_arg) DiscourseTagging.add_or_create_tags_by_name(self, tag_names_arg, unlimited: true) end @@ -379,33 +403,6 @@ SQL self.update_attributes(latest_topic_id: latest_topic_id, latest_post_id: latest_post_id) end - def self.resolve_permissions(permissions) - read_restricted = true - - everyone = Group::AUTO_GROUPS[:everyone] - full = CategoryGroup.permission_types[:full] - - mapped = permissions.map do |group, permission| - group = group.id if group.is_a?(Group) - - # subtle, using Group[] ensures the group exists in the DB - group = Group[group.to_sym].id unless group.is_a?(Integer) - permission = CategoryGroup.permission_types[permission] unless permission.is_a?(Integer) - - [group, permission] - end - - mapped.each do |group, permission| - if group == everyone && permission == full - return [false, []] - end - - read_restricted = false if group == everyone - end - - [read_restricted, mapped] - end - def self.query_parent_category(parent_slug) self.where(slug: parent_slug, parent_category_id: nil).pluck(:id).first || self.where(id: parent_slug.to_i).pluck(:id).first diff --git a/app/models/group.rb b/app/models/group.rb index 90bb845f63f..5d8e84436ec 100644 --- a/app/models/group.rb +++ b/app/models/group.rb @@ -434,6 +434,15 @@ class Group < ActiveRecord::Base end end + # given something that might be a group name, id, or record, return the group id + def self.group_id_from_param(group_param) + return group_param.id if group_param.is_a?(Group) + return group_param if group_param.is_a?(Integer) + + # subtle, using Group[] ensures the group exists in the DB + Group[group_param.to_sym].id + end + def self.builtin Enum.new(:moderators, :admins, :trust_level_1, :trust_level_2) end diff --git a/app/models/tag.rb b/app/models/tag.rb index f110e677ce6..d452c66559f 100644 --- a/app/models/tag.rb +++ b/app/models/tag.rb @@ -46,11 +46,14 @@ class Tag < ActiveRecord::Base return [] if scope_category_ids.empty? + filter_sql = guardian&.is_staff? ? '' : (' AND ' + DiscourseTagging.filter_visible_sql) + tag_names_with_counts = Tag.exec_sql <<~SQL SELECT tags.name as tag_name, SUM(stats.topic_count) AS sum_topic_count FROM category_tag_stats stats INNER JOIN tags ON stats.tag_id = tags.id AND stats.topic_count > 0 WHERE stats.category_id in (#{scope_category_ids.join(',')}) + #{filter_sql} GROUP BY tags.name ORDER BY sum_topic_count DESC, tag_name ASC LIMIT #{limit} diff --git a/app/models/tag_group.rb b/app/models/tag_group.rb index b1c7088c709..2ed60e07801 100644 --- a/app/models/tag_group.rb +++ b/app/models/tag_group.rb @@ -5,9 +5,14 @@ class TagGroup < ActiveRecord::Base has_many :tags, through: :tag_group_memberships has_many :category_tag_groups, dependent: :destroy has_many :categories, through: :category_tag_groups + has_many :tag_group_permissions, dependent: :destroy belongs_to :parent_tag, class_name: 'Tag' + before_save :apply_permissions + + attr_accessor :permissions + def tag_names=(tag_names_arg) DiscourseTagging.add_or_create_tags_by_name(self, tag_names_arg, unlimited: true) end @@ -22,13 +27,47 @@ class TagGroup < ActiveRecord::Base end end + def permissions=(permissions) + @permissions = TagGroup.resolve_permissions(permissions) + end + + def self.resolve_permissions(permissions) + everyone_group_id = Group::AUTO_GROUPS[:everyone] + full = TagGroupPermission.permission_types[:full] + + mapped = permissions.map do |group, permission| + group_id = Group.group_id_from_param(group) + permission = TagGroupPermission.permission_types[permission] unless permission.is_a?(Integer) + + return [] if group_id == everyone_group_id && permission == full + + [group_id, permission] + end + end + + def apply_permissions + if @permissions + tag_group_permissions.destroy_all + @permissions.each do |group_id, permission_type| + tag_group_permissions.build(group_id: group_id, permission_type: permission_type) + end + @permissions = nil + end + end + + def visible_only_to_staff + # currently only "everyone" and "staff" groups are supported + tag_group_permissions.count > 0 + end + def self.allowed(guardian) if guardian.is_staff? TagGroup else category_permissions_filter = <<~SQL - id IN ( SELECT tag_group_id FROM category_tag_groups WHERE category_id IN (?)) - OR id NOT IN (SELECT tag_group_id FROM category_tag_groups) + (id IN ( SELECT tag_group_id FROM category_tag_groups WHERE category_id IN (?)) + OR id NOT IN (SELECT tag_group_id FROM category_tag_groups)) + AND id NOT IN (SELECT tag_group_id FROM tag_group_permissions) SQL TagGroup.where(category_permissions_filter, guardian.allowed_category_ids) diff --git a/app/models/tag_group_permission.rb b/app/models/tag_group_permission.rb new file mode 100644 index 00000000000..2d77af228ff --- /dev/null +++ b/app/models/tag_group_permission.rb @@ -0,0 +1,9 @@ +# Who can see and use tags belonging to a tag group. +class TagGroupPermission < ActiveRecord::Base + belongs_to :tag_group + belongs_to :group + + def self.permission_types + @permission_types ||= Enum.new(full: 1) #, see: 2 + end +end diff --git a/app/serializers/concerns/topic_tags_mixin.rb b/app/serializers/concerns/topic_tags_mixin.rb index 6ad88b47ea2..c2dc1254d22 100644 --- a/app/serializers/concerns/topic_tags_mixin.rb +++ b/app/serializers/concerns/topic_tags_mixin.rb @@ -9,7 +9,7 @@ module TopicTagsMixin def tags # Calling method `pluck` along with `includes` causing N+1 queries - topic.tags.map(&:name) + DiscourseTagging.filter_visible(topic.tags, scope).map(&:name) end def topic diff --git a/app/serializers/post_revision_serializer.rb b/app/serializers/post_revision_serializer.rb index b86fda23794..0e0348cb39b 100644 --- a/app/serializers/post_revision_serializer.rb +++ b/app/serializers/post_revision_serializer.rb @@ -160,7 +160,11 @@ class PostRevisionSerializer < ApplicationSerializer end def tags_changes - { previous: previous["tags"], current: current["tags"] } + changes = { + previous: filter_visible_tags(previous["tags"]), + current: filter_visible_tags(current["tags"]) + } + changes[:previous] == changes[:current] ? nil : changes end def include_tags_changes? @@ -250,4 +254,9 @@ class PostRevisionSerializer < ApplicationSerializer object.user || Discourse.system_user end + def filter_visible_tags(tags) + @hidden_tag_names ||= DiscourseTagging.hidden_tag_names(scope) + tags.is_a?(Array) ? (tags - @hidden_tag_names) : tags + end + end diff --git a/app/serializers/tag_group_serializer.rb b/app/serializers/tag_group_serializer.rb index 62858aff200..e0a4734f246 100644 --- a/app/serializers/tag_group_serializer.rb +++ b/app/serializers/tag_group_serializer.rb @@ -1,5 +1,5 @@ class TagGroupSerializer < ApplicationSerializer - attributes :id, :name, :tag_names, :parent_tag_name, :one_per_topic + attributes :id, :name, :tag_names, :parent_tag_name, :one_per_topic, :visible_only_to_staff def tag_names object.tags.map(&:name).sort diff --git a/config/locales/client.en.yml b/config/locales/client.en.yml index 523163cd467..d3144ca4c26 100644 --- a/config/locales/client.en.yml +++ b/config/locales/client.en.yml @@ -2662,6 +2662,7 @@ en: save: "Save" delete: "Delete" confirm_delete: "Are you sure you want to delete this tag group?" + visible_only_to_staff: "Tags are visible only to staff" topics: none: diff --git a/db/migrate/20180323154826_create_tag_group_permissions.rb b/db/migrate/20180323154826_create_tag_group_permissions.rb new file mode 100644 index 00000000000..74abffefc85 --- /dev/null +++ b/db/migrate/20180323154826_create_tag_group_permissions.rb @@ -0,0 +1,10 @@ +class CreateTagGroupPermissions < ActiveRecord::Migration[5.1] + def change + create_table :tag_group_permissions do |t| + t.references :tag_group, null: false + t.references :group, null: false + t.integer :permission_type, default: 1, null: false + t.timestamps null: false + end + end +end diff --git a/lib/discourse_tagging.rb b/lib/discourse_tagging.rb index 758b8dcd75a..2ae28445d22 100644 --- a/lib/discourse_tagging.rb +++ b/lib/discourse_tagging.rb @@ -76,7 +76,6 @@ module DiscourseTagging term.gsub!("_", "\\_") term = clean_tag(term) query = query.where('tags.name like ?', "%#{term}%") - # query = query.where('tags.id NOT IN (?)', selected_tag_ids) unless selected_tag_ids.empty? end # Filters for category-specific tags: @@ -152,7 +151,36 @@ module DiscourseTagging end end - query + if guardian.nil? || guardian.is_staff? + query + else + filter_visible(query, guardian) + end + end + + def self.filter_visible(query, guardian = nil) + if !guardian&.is_staff? && TagGroupPermission.where(group_id: Group::AUTO_GROUPS[:staff]).exists? + query.where(filter_visible_sql) + else + query + end + end + + def self.filter_visible_sql + @filter_visible_sql ||= <<~SQL + tags.id NOT IN ( + SELECT tgm.tag_id + FROM tag_group_memberships tgm + INNER JOIN tag_group_permissions tgp + ON tgp.tag_group_id = tgm.tag_group_id + AND tgp.group_id = #{Group::AUTO_GROUPS[:staff]}) + SQL + end + + def self.hidden_tag_names(guardian = nil) + return [] if guardian&.is_staff? || !TagGroupPermission.where(group_id: Group::AUTO_GROUPS[:staff]).exists? + tag_group_ids = TagGroupPermission.where(group_id: Group::AUTO_GROUPS[:staff]).pluck(:tag_group_id) + Tag.includes(:tag_groups).where('tag_group_id in (?)', tag_group_ids).pluck(:name) end def self.clean_tag(tag) diff --git a/spec/components/discourse_tagging_spec.rb b/spec/components/discourse_tagging_spec.rb index ab15f0b0d26..adc6c05a879 100644 --- a/spec/components/discourse_tagging_spec.rb +++ b/spec/components/discourse_tagging_spec.rb @@ -7,11 +7,12 @@ require 'discourse_tagging' describe DiscourseTagging do + let(:admin) { Fabricate(:admin) } let(:user) { Fabricate(:user) } - let!(:tag1) { Fabricate(:tag, name: "tag1") } - let!(:tag2) { Fabricate(:tag, name: "tag2") } - let!(:tag3) { Fabricate(:tag, name: "tag3") } + let!(:tag1) { Fabricate(:tag, name: "fun") } + let!(:tag2) { Fabricate(:tag, name: "fun2") } + let!(:tag3) { Fabricate(:tag, name: "fun3") } before do SiteSetting.tagging_enabled = true @@ -25,7 +26,7 @@ describe DiscourseTagging do tags = DiscourseTagging.filter_allowed_tags(Tag.all, Guardian.new(user), selected_tags: [tag2.name], for_input: true, - term: 'tag' + term: 'fun' ).map(&:name) expect(tags).to contain_exactly(tag1.name, tag3.name) end @@ -37,6 +38,41 @@ describe DiscourseTagging do ).map(&:name) expect(tags).to contain_exactly(tag1.name, tag3.name) end + + context 'with tags visible only to staff' do + let(:hidden_tag) { Fabricate(:tag) } + let!(:staff_tag_group) { Fabricate(:tag_group, permissions: { "staff" => 1 }, tag_names: [hidden_tag.name]) } + + it 'should return all tags to staff' do + tags = DiscourseTagging.filter_allowed_tags(Tag.all, Guardian.new(admin)).to_a + expect(tags).to contain_exactly(tag1, tag2, tag3, hidden_tag) + expect(tags.size).to eq(4) + end + + it 'should not return hidden tag to non-staff' do + tags = DiscourseTagging.filter_allowed_tags(Tag.all, Guardian.new(user)).to_a + expect(tags).to contain_exactly(tag1, tag2, tag3) + expect(tags.size).to eq(3) + end + end + end + end + + describe 'filter_visible' do + let(:hidden_tag) { Fabricate(:tag) } + let!(:staff_tag_group) { Fabricate(:tag_group, permissions: { "staff" => 1 }, tag_names: [hidden_tag.name]) } + let(:topic) { Fabricate(:topic, tags: [tag1, tag2, tag3, hidden_tag]) } + + it 'returns all tags to staff' do + tags = DiscourseTagging.filter_visible(topic.tags, Guardian.new(admin)) + expect(tags.size).to eq(4) + expect(tags).to contain_exactly(tag1, tag2, tag3, hidden_tag) + end + + it 'does not return hidden tags to non-staff' do + tags = DiscourseTagging.filter_visible(topic.tags, Guardian.new(user)) + expect(tags.size).to eq(3) + expect(tags).to contain_exactly(tag1, tag2, tag3) end end end diff --git a/spec/discourse_tagging_spec.rb b/spec/components/discourse_tagging_spec.rb.rb similarity index 100% rename from spec/discourse_tagging_spec.rb rename to spec/components/discourse_tagging_spec.rb.rb diff --git a/spec/models/tag_spec.rb b/spec/models/tag_spec.rb index 1d792c10a22..d915429fab1 100644 --- a/spec/models/tag_spec.rb +++ b/spec/models/tag_spec.rb @@ -94,6 +94,24 @@ describe Tag do expect(described_class.top_tags.sort).to eq([@tags[0].name, @tags[1].name, @tags[2].name].sort) end end + + context "with hidden tags" do + let(:hidden_tag) { Fabricate(:tag, name: 'hidden') } + let!(:staff_tag_group) { Fabricate(:tag_group, permissions: { "staff" => 1 }, tag_names: [hidden_tag.name]) } + let!(:topic2) { Fabricate(:topic, tags: [tag, hidden_tag]) } + + it "returns all tags to staff" do + expect(Tag.top_tags(guardian: Guardian.new(Fabricate(:admin)))).to include(hidden_tag.name) + end + + it "doesn't return hidden tags to anon" do + expect(Tag.top_tags).to_not include(hidden_tag.name) + end + + it "doesn't return hidden tags to non-staff" do + expect(Tag.top_tags(guardian: Guardian.new(Fabricate(:user)))).to_not include(hidden_tag.name) + end + end end describe '#pm_tags' do diff --git a/spec/serializers/post_revision_serializer_spec.rb b/spec/serializers/post_revision_serializer_spec.rb new file mode 100644 index 00000000000..9bed7723e00 --- /dev/null +++ b/spec/serializers/post_revision_serializer_spec.rb @@ -0,0 +1,53 @@ +require 'rails_helper' + +describe PostRevisionSerializer do + let(:post) { Fabricate(:post, version: 2) } + + context 'hidden tags' do + let(:public_tag) { Fabricate(:tag, name: 'public') } + let(:public_tag2) { Fabricate(:tag, name: 'visible') } + let(:hidden_tag) { Fabricate(:tag, name: 'hidden') } + let(:hidden_tag2) { Fabricate(:tag, name: 'secret') } + + let(:staff_tag_group) { Fabricate(:tag_group, permissions: { "staff" => 1 }, tag_names: [hidden_tag.name, hidden_tag2.name]) } + + let(:post_revision) do + Fabricate(:post_revision, + post: post, + modifications: { 'tags' => [['public', 'hidden'], ['visible', 'hidden']] } + ) + end + + let(:post_revision2) do + Fabricate(:post_revision, + post: post, + modifications: { 'tags' => [['visible', 'hidden', 'secret'], ['visible', 'hidden']] } + ) + end + + before do + SiteSetting.tagging_enabled = true + staff_tag_group + post.topic.tags = [public_tag2, hidden_tag] + end + + it 'returns all tag changes to staff' do + json = PostRevisionSerializer.new(post_revision, scope: Guardian.new(Fabricate(:admin)), root: false).as_json + expect(json[:tags_changes][:previous]).to include(public_tag.name) + expect(json[:tags_changes][:previous]).to include(hidden_tag.name) + expect(json[:tags_changes][:current]).to include(public_tag2.name) + expect(json[:tags_changes][:current]).to include(hidden_tag.name) + end + + it 'does not return hidden tags to non-staff' do + json = PostRevisionSerializer.new(post_revision, scope: Guardian.new(Fabricate(:user)), root: false).as_json + expect(json[:tags_changes][:previous]).to eq([public_tag.name]) + expect(json[:tags_changes][:current]).to eq([public_tag2.name]) + end + + it 'does not show tag modificiatons if changes are not visible to the user' do + json = PostRevisionSerializer.new(post_revision2, scope: Guardian.new(Fabricate(:user)), root: false).as_json + expect(json[:tags_changes]).to_not be_present + end + end +end diff --git a/spec/serializers/suggested_topic_serializer_spec.rb b/spec/serializers/suggested_topic_serializer_spec.rb index b9fcdb6d52f..da29757bf96 100644 --- a/spec/serializers/suggested_topic_serializer_spec.rb +++ b/spec/serializers/suggested_topic_serializer_spec.rb @@ -2,11 +2,12 @@ require 'rails_helper' describe SuggestedTopicSerializer do let(:user) { Fabricate(:user) } + let(:admin) { Fabricate(:admin) } describe '#featured_link and #featured_link_root_domain' do let(:featured_link) { 'http://meta.discourse.org' } let(:topic) { Fabricate(:topic, featured_link: featured_link, category: Fabricate(:category, topic_featured_link_allowed: true)) } - subject(:json) { described_class.new(topic, scope: Guardian.new(user), root: false).as_json } + subject(:json) { SuggestedTopicSerializer.new(topic, scope: Guardian.new(user), root: false).as_json } context 'when topic featured link is disable' do before do @@ -32,4 +33,26 @@ describe SuggestedTopicSerializer do end end end + + describe 'hidden tags' do + let(:topic) { Fabricate(:topic) } + let(:hidden_tag) { Fabricate(:tag, name: 'hidden') } + let(:staff_tag_group) { Fabricate(:tag_group, permissions: { "staff" => 1 }, tag_names: [hidden_tag.name]) } + + before do + SiteSetting.tagging_enabled = true + staff_tag_group + topic.tags << hidden_tag + end + + it 'returns hidden tag to staff' do + json = SuggestedTopicSerializer.new(topic, scope: Guardian.new(admin), root: false).as_json + expect(json[:tags]).to eq([hidden_tag.name]) + end + + it 'does not return hidden tag to non-staff' do + json = SuggestedTopicSerializer.new(topic, scope: Guardian.new(user), root: false).as_json + expect(json[:tags]).to eq([]) + end + end end diff --git a/spec/serializers/topic_list_item_serializer_spec.rb b/spec/serializers/topic_list_item_serializer_spec.rb index 86f599e30d3..319d8f02c6e 100644 --- a/spec/serializers/topic_list_item_serializer_spec.rb +++ b/spec/serializers/topic_list_item_serializer_spec.rb @@ -43,4 +43,25 @@ describe TopicListItemSerializer do expect(serialized[:featured_link_root_domain]).to eq(nil) end end + + describe 'hidden tags' do + let(:hidden_tag) { Fabricate(:tag, name: 'hidden') } + let(:staff_tag_group) { Fabricate(:tag_group, permissions: { "staff" => 1 }, tag_names: [hidden_tag.name]) } + + before do + SiteSetting.tagging_enabled = true + staff_tag_group + topic.tags << hidden_tag + end + + it 'returns hidden tag to staff' do + json = TopicListItemSerializer.new(topic, scope: Guardian.new(Fabricate(:admin)), root: false).as_json + expect(json[:tags]).to eq([hidden_tag.name]) + end + + it 'does not return hidden tag to non-staff' do + json = TopicListItemSerializer.new(topic, scope: Guardian.new(Fabricate(:user)), root: false).as_json + expect(json[:tags]).to eq([]) + end + end end diff --git a/spec/serializers/topic_view_serializer_spec.rb b/spec/serializers/topic_view_serializer_spec.rb index a4b88a1a1c0..8a6e27b0561 100644 --- a/spec/serializers/topic_view_serializer_spec.rb +++ b/spec/serializers/topic_view_serializer_spec.rb @@ -1,13 +1,14 @@ require 'rails_helper' describe TopicViewSerializer do - def serialize_topic(topic, user) - topic_view = TopicView.new(topic.id, user) - described_class.new(topic_view, scope: Guardian.new(user), root: false).as_json + def serialize_topic(topic, user_arg) + topic_view = TopicView.new(topic.id, user_arg) + described_class.new(topic_view, scope: Guardian.new(user_arg), root: false).as_json end let(:topic) { Fabricate(:topic) } let(:user) { Fabricate(:user) } + let(:admin) { Fabricate(:admin) } describe '#featured_link and #featured_link_root_domain' do let(:featured_link) { 'http://meta.discourse.org' } @@ -69,7 +70,6 @@ describe TopicViewSerializer do describe 'when tags added to private message topics' do let(:moderator) { Fabricate(:moderator) } - let(:admin) { Fabricate(:admin) } let(:tag) { Fabricate(:tag) } let(:pm) do Fabricate(:private_message_topic, tags: [tag], topic_allowed_users: [ @@ -104,4 +104,25 @@ describe TopicViewSerializer do end end end + + describe 'with hidden tags' do + let(:hidden_tag) { Fabricate(:tag, name: 'hidden') } + let(:staff_tag_group) { Fabricate(:tag_group, permissions: { "staff" => 1 }, tag_names: [hidden_tag.name]) } + + before do + SiteSetting.tagging_enabled = true + staff_tag_group + topic.tags << hidden_tag + end + + it 'returns hidden tag to staff' do + json = serialize_topic(topic, admin) + expect(json[:tags]).to eq([hidden_tag.name]) + end + + it 'does not return hidden tag to non-staff' do + json = serialize_topic(topic, user) + expect(json[:tags]).to eq([]) + end + end end