0
0
Fork 0
mirror of https://github.com/discourse/discourse.git synced 2026-08-06 04:48:33 +08:00
discourse/spec/models/flag_spec.rb
Régis Hanol 41db2f76f3
FIX: Ensure each flag gets a unique name_key (#40899)
Previously, flag names with no ASCII word characters (e.g. Chinese) all
normalized to the same `name_key` of `custom_`, because `set_name_key`
stripped non-`\w` characters and Ruby's `\w` is ASCII-only. The flag
system keys lookups by `name_key` (the `disabled_flag_types` enum, the
frontend `actionByName` map), so once two flags shared a key, disabling
one of them flipped `can_act` for the whole group and hid every other
flag that shared it.

This makes `name_key` reliably unique:

- `set_name_key` falls back to `custom_flag` when the slug is empty and
appends a counter (`_2`, `_3`, ...) on collision, so distinct names
always produce distinct keys. It now only re-derives the key when the
name actually changes, keeping it stable across other saves.
- A post-deploy migration backfills existing duplicate keys and adds the
unique index on `flags.name_key` that the original `create_flags`
migration intended (`unique: true` is a no-op in `create_table`).

https://meta.discourse.org/t/405254
2026-06-24 16:37:59 +02:00

134 lines
4 KiB
Ruby
Vendored

# frozen_string_literal: true
RSpec.describe Flag, type: :model do
after(:each) { Flag.reset_flag_settings! }
it "has id lower than 1000 for system flags" do
flag = Fabricate(:flag, id: 1)
expect(flag.system?).to be true
flag.destroy!
end
it "has id greater than 1000 for non-system flags" do
flag = Fabricate(:flag)
expect(flag.system?).to be false
expect(flag.id).to be > 1000
flag.destroy!
end
it "has correct name key" do
flag = Fabricate(:flag, name: "FlAg!!!")
expect(flag.name_key).to eq("custom_flag")
flag.update!(name: "It's Illegal")
expect(flag.name_key).to eq("custom_its_illegal")
flag.update!(name: "THIS IS SPaM!+)(*&^%$#@@@!)")
expect(flag.name_key).to eq("custom_this_is_spam")
flag.destroy!
end
it "generates a unique, non-empty name key for non-ASCII names" do
flag1 = Fabricate(:flag, name: "测试举报一")
flag2 = Fabricate(:flag, name: "测试举报二")
flag3 = Fabricate(:flag, name: "测试举报三")
keys = [flag1, flag2, flag3].map(&:name_key)
expect(keys).to eq(%w[custom_flag custom_flag_2 custom_flag_3])
expect(keys.uniq.size).to eq(3)
[flag1, flag2, flag3].each(&:destroy!)
end
it "generates a unique name key when distinct names normalize identically" do
flag1 = Fabricate(:flag, name: "Spam!")
flag2 = Fabricate(:flag, name: "Spam?")
expect(flag1.name_key).to eq("custom_spam")
expect(flag2.name_key).to eq("custom_spam_2")
[flag1, flag2].each(&:destroy!)
end
it "keeps the name key stable when the name does not change" do
flag = Fabricate(:flag, name: "测试举报一")
expect(flag.name_key).to eq("custom_flag")
flag.update!(enabled: false)
expect(flag.reload.name_key).to eq("custom_flag")
flag.destroy!
end
it "updates post action types when created, modified or destroyed" do
expect(PostActionType.flag_types.keys).to eq(
%i[notify_user off_topic inappropriate spam illegal notify_moderators],
)
expect(ReviewableScore.types.keys).to eq(
%i[notify_user off_topic inappropriate spam illegal notify_moderators needs_approval],
)
flag = Fabricate(:flag, name: "flag")
expect(PostActionType.flag_types.keys).to eq(
%i[notify_user off_topic inappropriate spam illegal notify_moderators custom_flag],
)
expect(ReviewableScore.types.keys).to eq(
%i[
notify_user
off_topic
inappropriate
spam
illegal
notify_moderators
custom_flag
needs_approval
],
)
flag.update!(name: "edited_flag")
expect(PostActionType.flag_types.keys).to eq(
%i[notify_user off_topic inappropriate spam illegal notify_moderators custom_edited_flag],
)
expect(ReviewableScore.types.keys).to eq(
%i[
notify_user
off_topic
inappropriate
spam
illegal
notify_moderators
custom_edited_flag
needs_approval
],
)
flag.destroy!
expect(PostActionType.flag_types.keys).to eq(
%i[notify_user off_topic inappropriate spam illegal notify_moderators],
)
expect(ReviewableScore.types.keys).to eq(
%i[notify_user off_topic inappropriate spam illegal notify_moderators needs_approval],
)
end
describe ".used_flag_ids" do
fab!(:used_by_post_action_flag, :flag)
fab!(:used_by_reviewable_score_flag, :flag)
fab!(:unused_flag, :flag)
fab!(:post_action) { Fabricate(:post_action, post_action_type_id: used_by_post_action_flag.id) }
fab!(:reviewable_score) do
Fabricate(:reviewable_score, reviewable_score_type: used_by_reviewable_score_flag.id)
end
it "returns the ids of flags that are associated to a `PostAction` or `ReviewableScore`" do
expect(
Flag.used_flag_ids(
[used_by_post_action_flag.id, used_by_reviewable_score_flag.id, unused_flag.id],
),
).to contain_exactly(used_by_post_action_flag.id, used_by_reviewable_score_flag.id)
end
end
end