0
0
Fork 0
mirror of https://github.com/discourse/discourse.git synced 2026-08-04 10:39:43 +08:00
discourse/spec/serializers/user_serializer_spec.rb
Natalie Tay cff8f6ea4d
FEATURE: Allow selection of multiple content languages when viewing topics and posts (#42128)
Related:
https://meta.discourse.org/t/multi-language-preferences-for-displaying-localized-content/381116

Allow anon and logged in users to set more than one language that they
understand, when content localization is enabled.


### User preferences

In user preferences, we now include a Content Languages section when the
site has Content Localization enabled.

### Topic menu

In topics where there are any languages that's not the user's language,
the option will appear to set their preferred content languages. This
appears for anon and logged in users, but anon users will be asked to
logged in to set the languages.
__________

These preferences apply only to topic titles and posts. They do not
affect navigational features like categories, tags, or sidebar content.
The per-post option to show the original remains independent of the
user's preferences.

This PR temporarily retains `show_original_content` as a compatibility
field computed from `automatically_translate`, keeping cached clients
and rolling deployments safe. A follow-up PR will remove the legacy API
and frontend compatibility code, then retire the old database column
through the staged column-removal process.
2026-07-30 12:19:44 +08:00

692 lines
22 KiB
Ruby
Vendored

# frozen_string_literal: true
RSpec.describe UserSerializer do
fab!(:user) { Fabricate(:user, trust_level: 0) }
before { user.user_stat.update!(post_count: 1, topic_count: 2) }
context "with a TL0 user seen as anonymous" do
let(:serializer) { UserSerializer.new(user, scope: Guardian.new, root: false) }
let(:json) { serializer.as_json }
let(:untrusted_attributes) do
%i[
bio_raw
bio_cooked
bio_excerpt
location
website
website_name
profile_background
card_background
]
end
it "doesn't serialize untrusted attributes" do
untrusted_attributes.each { |attr| expect(json).not_to have_key(attr) }
end
it "serializes correctly" do
expect(json[:group_users]).to eq(nil)
expect(json[:second_factor_enabled]).to eq(nil)
end
end
context "as moderator" do
it "serializes correctly" do
json =
UserSerializer.new(user, scope: Guardian.new(Fabricate(:moderator)), root: false).as_json
expect(json[:group_users]).to eq(nil)
expect(json[:second_factor_enabled]).to eq(nil)
end
end
context "as current user" do
it "serializes options correctly" do
# so we serialize more stuff
SiteSetting.default_other_auto_track_topics_after_msecs = 0
SiteSetting.default_other_notification_level_when_replying = 3
SiteSetting.default_other_new_topic_duration_minutes = 60 * 24
user = Fabricate(:user)
user.user_option.update(dynamic_favicon: true, skip_new_user_tips: true)
json = UserSerializer.new(user, scope: Guardian.new(user), root: false).as_json
expect(json[:user_option][:dynamic_favicon]).to eq(true)
expect(json[:user_option][:skip_new_user_tips]).to eq(true)
expect(json[:user_option][:new_topic_duration_minutes]).to eq(60 * 24)
expect(json[:user_option][:auto_track_topics_after_msecs]).to eq(0)
expect(json[:user_option][:notification_level_when_replying]).to eq(3)
expect(json[:group_users]).to eq([])
expect(json[:second_factor_enabled]).to eq(false)
end
it "computes the legacy localization preference from automatically_translate" do
user.user_option.automatically_translate = false
user.user_option.show_original_content = false
json = UserSerializer.new(user, scope: Guardian.new(user), root: false).as_json
expect(json[:user_option]).to include(
automatically_translate: false,
show_original_content: true,
)
end
end
context "with a user" do
let(:admin_user) { Fabricate(:admin) }
let(:scope) { Guardian.new }
fab!(:user)
let(:serializer) { UserSerializer.new(user, scope: scope, root: false) }
let(:json) { serializer.as_json }
fab!(:upload)
fab!(:upload2, :upload)
context "when the scope user is admin" do
let(:scope) { Guardian.new(admin_user) }
it "returns the user's category notification levels, not the scope user's" do
category1 = Fabricate(:category)
category2 = Fabricate(:category)
category3 = Fabricate(:category)
category4 = Fabricate(:category)
CategoryUser.create(
category: category1,
user: user,
notification_level: CategoryUser.notification_levels[:muted],
)
CategoryUser.create(
category: Fabricate(:category),
user: admin_user,
notification_level: CategoryUser.notification_levels[:muted],
)
CategoryUser.create(
category: category2,
user: user,
notification_level: CategoryUser.notification_levels[:tracking],
)
CategoryUser.create(
category: Fabricate(:category),
user: admin_user,
notification_level: CategoryUser.notification_levels[:tracking],
)
CategoryUser.create(
category: category3,
user: user,
notification_level: CategoryUser.notification_levels[:watching],
)
CategoryUser.create(
category: Fabricate(:category),
user: admin_user,
notification_level: CategoryUser.notification_levels[:watching],
)
CategoryUser.create(
category: category4,
user: user,
notification_level: CategoryUser.notification_levels[:regular],
)
CategoryUser.create(
category: Fabricate(:category),
user: admin_user,
notification_level: CategoryUser.notification_levels[:regular],
)
expect(json[:muted_category_ids]).to eq([category1.id])
expect(json[:tracked_category_ids]).to eq([category2.id])
expect(json[:watched_category_ids]).to eq([category3.id])
expect(json[:regular_category_ids]).to eq([category4.id])
end
it "returns the user's tag notification levels, not the scope user's" do
tag1 = Fabricate(:tag)
tag2 = Fabricate(:tag)
tag3 = Fabricate(:tag)
tag4 = Fabricate(:tag)
TagUser.create(
tag: tag1,
user: user,
notification_level: TagUser.notification_levels[:muted],
)
TagUser.create(
tag: Fabricate(:tag),
user: admin_user,
notification_level: TagUser.notification_levels[:muted],
)
TagUser.create(
tag: tag2,
user: user,
notification_level: TagUser.notification_levels[:tracking],
)
TagUser.create(
tag: Fabricate(:tag),
user: admin_user,
notification_level: TagUser.notification_levels[:tracking],
)
TagUser.create(
tag: tag3,
user: user,
notification_level: TagUser.notification_levels[:watching],
)
TagUser.create(
tag: Fabricate(:tag),
user: admin_user,
notification_level: TagUser.notification_levels[:watching],
)
TagUser.create(
tag: tag4,
user: user,
notification_level: TagUser.notification_levels[:watching_first_post],
)
TagUser.create(
tag: Fabricate(:tag),
user: admin_user,
notification_level: TagUser.notification_levels[:watching_first_post],
)
expect(json[:muted_tags]).to eq([{ id: tag1.id, name: tag1.name, slug: tag1.slug }])
expect(json[:tracked_tags]).to eq([{ id: tag2.id, name: tag2.name, slug: tag2.slug }])
expect(json[:watched_tags]).to eq([{ id: tag3.id, name: tag3.name, slug: tag3.slug }])
expect(json[:watching_first_post_tags]).to eq(
[{ id: tag4.id, name: tag4.name, slug: tag4.slug }],
)
end
end
context "with staff attributes" do
it "includes staff attributes for staff" do
json = UserSerializer.new(user, scope: Guardian.new(Fabricate(:admin)), root: false).as_json
expect(json[:post_count]).to eq(1)
expect(json[:topic_count]).to eq(2)
expect(json).to have_key(:can_be_deleted)
expect(json).to have_key(:can_delete_all_posts)
end
it "does not include staff attributes for non-staff" do
json = UserSerializer.new(user, scope: Guardian.new, root: false).as_json
expect(json).not_to have_key(:post_count)
expect(json).not_to have_key(:topic_count)
expect(json).not_to have_key(:can_be_deleted)
expect(json).not_to have_key(:can_delete_all_posts)
end
end
context "with `enable_names` true" do
before { SiteSetting.enable_names = true }
it "has a name" do
expect(json[:name]).to be_present
end
end
context "with `enable_names` false" do
before { SiteSetting.enable_names = false }
it "has a name" do
expect(json[:name]).to be_blank
end
end
context "with filled out backgrounds" do
before do
user.user_profile.upload_card_background(upload)
user.user_profile.upload_profile_background(upload2)
end
it "has a profile background" do
expect(json[:card_background_upload_url]).to eq(upload.url)
expect(json[:profile_background_upload_url]).to eq(upload2.url)
end
end
context "with filled out website" do
context "when website has a path" do
before { user.user_profile.website = "http://example.com/user" }
it "has a website with a path" do
expect(json[:website]).to eq "http://example.com/user"
end
it "returns complete website name with path" do
expect(json[:website_name]).to eq "example.com/user"
end
end
context "when website has a subdomain" do
before { user.user_profile.website = "http://subdomain.example.com/user" }
it "has a website with a subdomain" do
expect(json[:website]).to eq "http://subdomain.example.com/user"
end
it "returns website name with the subdomain" do
expect(json[:website_name]).to eq "subdomain.example.com/user"
end
end
context "when website has www" do
before { user.user_profile.website = "http://www.example.com/user" }
it "has a website with the www" do
expect(json[:website]).to eq "http://www.example.com/user"
end
it "returns website name without the www" do
expect(json[:website_name]).to eq "example.com/user"
end
end
context "when website includes query parameters" do
before { user.user_profile.website = "http://example.com/user?ref=payme" }
it "has a website with query params" do
expect(json[:website]).to eq "http://example.com/user?ref=payme"
end
it "has a website name without query params" do
expect(json[:website_name]).to eq "example.com/user"
end
end
context "when website is not a valid url" do
before { user.user_profile.website = "invalid-url" }
it "has a website with the invalid url" do
expect(json[:website]).to eq "invalid-url"
end
it "has a nil website name" do
expect(json[:website_name]).to eq nil
end
end
end
context "with filled out bio" do
before do
user.user_profile.bio_raw = "my raw bio"
user.user_profile.bio_cooked = "my cooked bio"
end
it "has a bio" do
expect(json[:bio_raw]).to eq "my raw bio"
end
it "has a cooked bio" do
expect(json[:bio_cooked]).to eq "my cooked bio"
end
end
describe "second_factor_enabled" do
let(:scope) { Guardian.new(user) }
it "is false by default" do
expect(json[:second_factor_enabled]).to eq(false)
end
context "when totp enabled" do
before { User.any_instance.stubs(:totp_enabled?).returns(true) }
it "is true" do
expect(json[:second_factor_enabled]).to eq(true)
end
end
context "when security_keys enabled" do
before { User.any_instance.stubs(:security_keys_enabled?).returns(true) }
it "is true" do
expect(json[:second_factor_enabled]).to eq(true)
end
end
end
describe "ignored and muted" do
fab!(:viewing_user, :user)
let(:scope) { Guardian.new(viewing_user) }
it "returns false values for muted and ignored" do
expect(json[:ignored]).to eq(false)
expect(json[:muted]).to eq(false)
end
context "when ignored" do
before do
Fabricate(:ignored_user, user: viewing_user, ignored_user: user)
viewing_user.reload
end
it "returns true for ignored" do
expect(json[:ignored]).to eq(true)
expect(json[:muted]).to eq(false)
end
end
context "when muted" do
before do
Fabricate(:muted_user, user: viewing_user, muted_user: user)
viewing_user.reload
end
it "returns true for muted" do
expect(json[:muted]).to eq(true)
expect(json[:ignored]).to eq(false)
end
end
end
describe "with a custom notification schedule" do
let(:schedule) do
UserNotificationSchedule.create({ user: user }.merge(UserNotificationSchedule::DEFAULT))
end
let(:scope) { Guardian.new(user) }
it "includes the serialized schedule" do
expect(json[:user_notification_schedule][:enabled]).to eq(schedule[:enabled])
expect(json[:user_notification_schedule][:day_0_start_time]).to eq(
schedule[:day_0_start_time],
)
expect(json[:user_notification_schedule][:day_6_end_time]).to eq(schedule[:day_6_end_time])
end
end
end
context "with custom_fields" do
fab!(:user)
let(:json) { UserSerializer.new(user, scope: Guardian.new, root: false).as_json }
before do
user.custom_fields["secret_field"] = "Only for me to know"
user.custom_fields["public_field"] = "Everyone look here"
user.save
end
it "doesn't serialize the fields by default" do
json[:custom_fields]
expect(json[:custom_fields]).to be_empty
end
it "serializes the fields listed in public_user_custom_fields site setting" do
SiteSetting.public_user_custom_fields = "public_field"
expect(json[:custom_fields]["public_field"]).to eq(user.custom_fields["public_field"])
expect(json[:custom_fields]["secret_field"]).to eq(nil)
end
context "with user custom field" do
before do
plugin = Plugin::Instance.new
plugin.allow_public_user_custom_field :public_field
end
after { DiscoursePluginRegistry.reset_register!(:public_user_custom_fields) }
it "serializes the fields listed in public_user_custom_fields" do
expect(json[:custom_fields]["public_field"]).to eq(user.custom_fields["public_field"])
expect(json[:custom_fields]["secret_field"]).to eq(nil)
end
end
end
context "with user fields" do
fab!(:user)
let! :fields do
[
Fabricate(:user_field),
Fabricate(:user_field),
Fabricate(:user_field, show_on_profile: true),
Fabricate(:user_field, show_on_user_card: true),
Fabricate(:user_field, show_on_user_card: true, show_on_profile: true),
]
end
let(:other_user_json) do
UserSerializer.new(user, scope: Guardian.new(Fabricate(:user)), root: false).as_json
end
let(:self_json) { UserSerializer.new(user, scope: Guardian.new(user), root: false).as_json }
let(:admin_json) do
UserSerializer.new(user, scope: Guardian.new(Fabricate(:admin)), root: false).as_json
end
it "includes the correct fields for each audience" do
expect(admin_json[:user_fields].keys).to contain_exactly(
*fields.map { |field| field.id.to_s },
)
expect(other_user_json[:user_fields].keys).to contain_exactly(
*fields[2..5].map { |field| field.id.to_s },
)
expect(self_json[:user_fields].keys).to contain_exactly(*fields.map { |field| field.id.to_s })
end
end
context "when public profiles are hidden from anonymous viewers" do
fab!(:user)
fab!(:user_field) { Fabricate(:user_field, show_on_profile: true) }
before do
SiteSetting.hide_user_profiles_from_public = true
user.user_stat.update!(post_count: 1)
user.user_profile.update!(
bio_raw: "private bio",
bio_cooked: "private cooked bio",
location: "private location",
website: "https://example.com/private",
)
user.set_user_field(user_field.id, "private field")
end
it "does not serialize profile details" do
json = UserSerializer.new(user, scope: Guardian.new, root: false).as_json
expect(json.keys).not_to include(
:bio_raw,
:bio_cooked,
:bio_excerpt,
:location,
:website,
:website_name,
:user_fields,
)
end
end
context "with user_api_keys" do
fab!(:user)
it "sorts keys by last used time" do
freeze_time
user_api_key_0 =
Fabricate(
:readonly_user_api_key,
user: user,
last_used_at: 2.days.ago,
revoked_at: Time.zone.now,
)
user_api_key_1 = Fabricate(:readonly_user_api_key, user: user, last_used_at: 7.days.ago)
user_api_key_2 = Fabricate(:readonly_user_api_key, user: user, last_used_at: 1.day.ago)
user_api_key_3 =
Fabricate(
:readonly_user_api_key,
user: user,
last_used_at: 4.days.ago,
revoked_at: Time.zone.now,
)
user_api_key_4 = Fabricate(:readonly_user_api_key, user: user, last_used_at: 3.days.ago)
json = UserSerializer.new(user, scope: Guardian.new(user), root: false).as_json
expect(json[:user_api_keys].size).to eq(3)
expect(json[:user_api_keys][0][:id]).to eq(user_api_key_1.id)
expect(json[:user_api_keys][1][:id]).to eq(user_api_key_4.id)
expect(json[:user_api_keys][2][:id]).to eq(user_api_key_2.id)
end
it "includes expires_at" do
freeze_time
key = Fabricate(:readonly_user_api_key, user: user, expires_at: 1.day.from_now)
json = UserSerializer.new(user, scope: Guardian.new(user), root: false).as_json
serialized_key = json[:user_api_keys].find { |api_key| api_key[:id] == key.id }
expect(serialized_key[:expires_at]).to eq_time(1.day.from_now)
end
it "includes expired keys that have not been revoked" do
expired_key = Fabricate(:readonly_user_api_key, user: user, expires_at: 1.day.ago)
json = UserSerializer.new(user, scope: Guardian.new(user), root: false).as_json
expect(json[:user_api_keys].map { |key| key[:id] }).to include(expired_key.id)
end
end
context "with user_passkeys" do
fab!(:user)
fab!(:passkey0) do
Fabricate(:passkey_with_random_credential, user: user, created_at: 5.hours.ago)
end
fab!(:passkey1) do
Fabricate(:passkey_with_random_credential, user: user, created_at: 2.hours.ago)
end
it "does not include them if feature is disabled" do
SiteSetting.enable_passkeys = false
json = UserSerializer.new(user, scope: Guardian.new(user), root: false).as_json
expect(json[:user_passkeys]).to eq(nil)
end
it "does not include them if requesting user isn't current user" do
SiteSetting.enable_passkeys = true
json = UserSerializer.new(user, scope: Guardian.new(), root: false).as_json
expect(json[:user_passkeys]).to eq(nil)
end
it "includes passkeys if feature is enabled for current user" do
SiteSetting.enable_passkeys = true
json = UserSerializer.new(user, scope: Guardian.new(user), root: false).as_json
expect(json[:user_passkeys][0][:id]).to eq(passkey0.id)
expect(json[:user_passkeys][0][:name]).to eq(passkey0.name)
expect(json[:user_passkeys][0][:last_used]).to eq(passkey0.last_used)
expect(json[:user_passkeys][1][:id]).to eq(passkey1.id)
end
end
context "with no_password" do
fab!(:passwordless_user) { Fabricate(:user, password: nil) }
it "is not included for anonymous viewers" do
json = UserSerializer.new(passwordless_user, scope: Guardian.new, root: false).as_json
expect(json).not_to have_key(:no_password)
end
it "is not included when viewed by another non-staff user" do
json =
UserSerializer.new(
passwordless_user,
scope: Guardian.new(Fabricate(:user)),
root: false,
).as_json
expect(json).not_to have_key(:no_password)
end
it "is included when viewed by an admin" do
json =
UserSerializer.new(
passwordless_user,
scope: Guardian.new(Fabricate(:admin)),
root: false,
).as_json
expect(json[:no_password]).to eq(true)
end
it "is included when viewed by a moderator" do
json =
UserSerializer.new(
passwordless_user,
scope: Guardian.new(Fabricate(:moderator)),
root: false,
).as_json
expect(json[:no_password]).to eq(true)
end
it "is included when the user views their own profile and has no password" do
json =
UserSerializer.new(
passwordless_user,
scope: Guardian.new(passwordless_user),
root: false,
).as_json
expect(json[:no_password]).to eq(true)
end
it "is not included when the user views their own profile but has a password" do
json = UserSerializer.new(user, scope: Guardian.new(user), root: false).as_json
expect(json).not_to have_key(:no_password)
end
it "is not included for staff when the target user has a password" do
json = UserSerializer.new(user, scope: Guardian.new(Fabricate(:admin)), root: false).as_json
expect(json).not_to have_key(:no_password)
end
end
context "for user sidebar attributes" do
include_examples "User Sidebar Serializer Attributes", described_class
it "does not include attributes when scoped to user that cannot edit user" do
user2 = Fabricate(:user)
serializer = described_class.new(user, scope: Guardian.new(user2), root: false)
expect(serializer.as_json[:sidebar_category_ids]).to eq(nil)
expect(serializer.as_json[:sidebar_tags]).to eq(nil)
expect(serializer.as_json[:display_sidebar_tags]).to eq(nil)
end
end
context "with groups" do
fab!(:group) do
Fabricate(
:group,
visibility_level: Group.visibility_levels[:public],
members_visibility_level: Group.visibility_levels[:owners],
)
end
let(:serializer) { UserSerializer.new(user, scope: guardian, root: false) }
before do
group.add(user)
group.save!
end
context "when serializing user's own groups" do
let(:guardian) { Guardian.new(user) }
it "includes secret membership group" do
json = serializer.as_json
expect(json[:groups].map { |g| g[:id] }).to include(group.id)
end
end
context "when serializing other users' groups" do
let(:guardian) { Guardian.new }
it "does not include secret membership group" do
json = serializer.as_json
expect(json[:groups]).to be_empty
end
end
end
end