# frozen_string_literal: true #-- copyright # OpenProject is an open source project management software. # Copyright (C) the OpenProject GmbH # # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License version 3. # # OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows: # Copyright (C) 2006-2013 Jean-Philippe Lang # Copyright (C) 2010-2013 the ChiliProject Team # # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License # as published by the Free Software Foundation; either version 2 # of the License, or (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. # # See COPYRIGHT and LICENSE files for more details. #++ require "spec_helper" RSpec.describe User do let(:user) { build(:user) } let(:project) { create(:project_with_types) } let(:role) { create(:project_role, permissions: [:view_work_packages]) } let(:member) do build(:member, project:, roles: [role], principal: user) end let(:status) { create(:status) } let(:issue) do build(:work_package, type: project.types.first, author: user, project:, status:) end describe "Associations" do it { is_expected.to have_many(:emoji_reactions).dependent(:destroy) } it { is_expected.to have_many(:reminders).with_foreign_key(:creator_id).dependent(:destroy).inverse_of(:creator) } it { is_expected.to have_many(:oauth_grants).with_foreign_key(:resource_owner_id).dependent(:delete_all) } it { is_expected.to have_many(:oauth_applications).dependent(:destroy) } end describe "with long but allowed attributes" do it "is valid" do user.firstname = "a" * 256 user.lastname = "b" * 256 user.mail = "fo#{'o' * 237}@mail.example.com" expect(user).to be_valid expect(user.save).to be_truthy end end describe "a user with and overly long firstname (> 256 chars)" do it "is invalid" do user.firstname = "a" * 257 expect(user).not_to be_valid expect(user.save).to be_falsey end end describe "a user with and overly long lastname (> 256 chars)" do it "is invalid" do user.lastname = "a" * 257 expect(user).not_to be_valid expect(user.save).to be_falsey end end describe "#mail" do before do user.mail = mail end context "with whitespaces" do let(:mail) { " foo@bar.com " } it "is stripped" do expect(user.mail) .to eql "foo@bar.com" end end context "for local mail addresses" do let(:mail) { "foobar@abc.def.some-internet" } it "is valid" do expect(user).to be_valid end end context "for wrong mail addresses" do let(:mail) { "foobar+abc.def.some-internet" } it "is invalid" do expect(user).to be_invalid end end context "for an already taken mail addresses (different take)" do let(:mail) { "foo@bar.com" } let!(:other_user) { create(:user, mail: "Foo@Bar.com") } it "is invalid" do expect(user).to be_invalid end end end describe "#login" do before do user.login = login end context "with whitespace" do context "with simple spaces" do let(:login) { "a b c" } it "is valid" do expect(user).to be_valid end it "may be stored in the database" do expect(user.save).to be_truthy end end context "with line breaks" do let(:login) { 'ab\nc' } it "is invalid" do expect(user).not_to be_valid end it "may not be stored in the database" do expect(user.save).to be_falsey end end context "with other letter char classes" do let(:login) { "célîneüberölig" } it "is valid" do expect(user).to be_valid end it "may be stored in the database" do expect(user.save).to be_truthy end end context "with tabs" do let(:login) { 'ab\tc' } it "is invalid" do expect(user).not_to be_valid end it "may not be stored in the database" do expect(user.save).to be_falsey end end end context "with symbols" do %w[+ _ . - @].each do |symbol| context symbol do let(:login) { "foo#{symbol}bar" } it "is valid" do expect(user).to be_valid end it "may be stored in the database" do expect(user.save).to be_truthy end end end context "with combination thereof" do let(:login) { "the+boss-is-über@the_house." } it "is valid" do expect(user).to be_valid end it "may be stored in the database" do expect(user.save).to be_truthy end end context "with invalid symbol" do let(:login) { "invalid!name" } it "is invalid" do expect(user).not_to be_valid end it "may not be stored in the database" do expect(user.save).to be_falsey end end end context "with more that 255 chars" do let(:login) { "a" * 256 } it "is valid" do user.login = login expect(user).to be_valid end it "may be loaded from the database" do user.login = login expect(user.save).to be_truthy expect(described_class.find_by_login(login)).to eq(user) expect(described_class.find_by_unique(login)).to eq(user) end end context "with an invalid login" do let(:login) { "me" } it "is invalid" do user.login = login expect(user).not_to be_valid end end context "with an overly long login (> 256 chars)" do let(:login) { "a" * 257 } it "is invalid" do expect(user).not_to be_valid end it "may not be stored in the database" do expect(user.save).to be_falsey end end context "with another user having the login in a different case" do let!(:other_user) { create(:user, login: "NewUser") } let(:login) { "newuser" } it "is invalid" do expect(user).not_to be_valid end end context "when empty" do let(:login) { "" } it "is invalid" do expect(user).not_to be_valid end end end describe "name validation" do let(:user) do build(:user) end it "restricts some options", :aggregate_failures do [ "http://foobar.com", "", "https://hello.com" ].each do |name| user.firstname = name user.lastname = name expect(user).not_to be_valid expect(user.errors.symbols_for(:firstname)).to eq [:invalid] expect(user.errors.symbols_for(:lastname)).to eq [:invalid] end end it "allows a lot of options", :aggregate_failures do [ "Tim O'Reilly", "🔴Emojinames", "山本由紀夫", "Татьяна", "Users with spaces", "Müller, Phd.", "@invited+user.com", "Foo & Bar", "T’Oole" ].each do |name| user.firstname = name user.lastname = name expect(user).to be_valid end end end describe "#name" do before do create(:user, firstname: "John", lastname: "Smith", login: "username", mail: "user@name.org") end context "when formatting according to setting" do subject { user.name } let(:user) { described_class.select_for_name.last } context "for firstname_lastname", with_settings: { user_format: :firstname_lastname } do it { is_expected.to eq "John Smith" } end context "for firstname", with_settings: { user_format: :firstname } do it { is_expected.to eq "John" } end context "for lastname_firstname", with_settings: { user_format: :lastname_firstname } do it { is_expected.to eq "Smith John" } end context "for lastname_n_firstname", with_settings: { user_format: :lastname_n_firstname } do it { is_expected.to eq "SmithJohn" } end context "for lastname_comma_firstname", with_settings: { user_format: :lastname_comma_firstname } do it { is_expected.to eq "Smith, John" } end context "for username", with_settings: { user_format: :username } do it { is_expected.to eq "username" } end context "for nil", with_settings: { user_format: nil } do it { is_expected.to eq "John Smith" } end end context "when specifying format explicitly" do subject { user.name(formatter) } let(:user) { described_class.select_for_name(formatter).last } context "for lastname_comma_firstname" do let(:formatter) { :lastname_comma_firstname } it { is_expected.to eq "Smith, John" } end context "for username", with_settings: { user_format: :username } do let(:formatter) { :username } it { is_expected.to eq "username" } end end end describe "#authentication_provider" do let!(:authentication_provider) { create(:oidc_provider) } context "when there is a link between user and auth provider" do let(:user) { create(:user, authentication_provider:) } it "returns the provider when there is a link" do expect(user.authentication_provider).to eql(authentication_provider) end end context "when there is no link" do it "returns nil" do expect(user.authentication_provider).to be_nil end end end describe "#human_authentication_provider" do let!(:authentication_provider) { create(:oidc_provider) } context "when there is a link between user and auth provider" do let(:user) { create(:user, authentication_provider:) } it "returns a human readable name" do expect(user.human_authentication_provider).to eql(authentication_provider.display_name) end end context "when no provider exists" do it "returns nil" do expect(user.human_authentication_provider).to be_nil end end end describe "#blocked" do let!(:blocked_user) do create(:user, failed_login_count: 3, last_failed_login_on: Time.zone.now) end before do user.save! allow(Setting).to receive(:brute_force_block_after_failed_logins).and_return(3) allow(Setting).to receive(:brute_force_block_minutes).and_return(30) end it "returns the single blocked user" do expect(described_class.blocked.length).to eq(1) expect(described_class.blocked.first.id).to eq(blocked_user.id) end end describe "#change_password_allowed?" do let(:user) { build(:user) } context "for user without auth source" do before do user.ldap_auth_source = nil end it "is true" do assert user.change_password_allowed? end end context "for user with an auth source" do let(:auth_source) { create(:ldap_auth_source) } before do user.ldap_auth_source = auth_source end it "does not allow password changes" do expect(user).not_to be_change_password_allowed end end context "for user without LdapAuthSource and with external authentication" do before do user.ldap_auth_source = nil allow(user).to receive(:uses_external_authentication?).and_return(true) end it "does not allow a password change" do expect(user).not_to be_change_password_allowed end end end describe "#watches" do before do user.save! end describe "WHEN the user is watching" do let(:watcher) do Watcher.new(watchable: issue, user:) end before do issue.save! member.save! user.reload # the user object needs to know of its membership for the watcher to be valid watcher.save! end it { expect(user.watches).to eq([watcher]) } end describe "WHEN the user isn't watching" do before do issue.save! end it { expect(user.watches).to eq([]) } end end describe "#uses_external_authentication?" do context "with identity_url" do let(:user) { create(:user, identity_url: "test_provider:veryuniqueid") } it "returns true" do expect(user).to be_uses_external_authentication end end context "without identity_url" do let(:user) { build(:user, identity_url: nil) } it "returns false" do expect(user).not_to be_uses_external_authentication end end end describe "user create with empty password" do let(:user) { described_class.new(firstname: "new", lastname: "user", mail: "newuser@somenet.foo") } before do user.login = "new_user" user.password = "" user.password_confirmation = "" user.save end it { expect(user).not_to be_valid } it { expect(user.errors[:password]).to include I18n.t("activerecord.errors.messages.too_short", count: Setting.password_min_length.to_i) } end describe "#random_password" do let(:user) { described_class.new } context "without generation" do it { expect(user.password).to be_nil } it { expect(user.password_confirmation).to be_nil } end context "with generation" do before do user.random_password! end it { expect(user.password).not_to be_blank } it { expect(user.password_confirmation).not_to be_blank } it { expect(user.force_password_change).to be_truthy } end end describe "#try_authentication_for_existing_user" do def build_user_double_with_expired_password(is_expired) user_double = double("User") allow(user_double).to receive(:check_password?).and_return(true) allow(user_double).to receive(:active?).and_return(true) allow(user_double).to receive(:ldap_auth_source).and_return(nil) allow(user_double).to receive(:force_password_change).and_return(false) # check for expired password should always happen expect(user_double).to receive(:password_expired?) { is_expired } user_double end it "does not allow login with an expired password" do user_double = build_user_double_with_expired_password(true) # use !! to ensure value is boolean expect(!!described_class.try_authentication_for_existing_user(user_double, "anypassword")).to \ be(false) end it "allows login with a not expired password" do user_double = build_user_double_with_expired_password(false) # use !! to ensure value is boolean expect(!!described_class.try_authentication_for_existing_user(user_double, "anypassword")).to \ be(true) end context "with an external auth source" do let(:auth_source) { build(:ldap_auth_source) } let(:user_with_external_auth_source) do user = build(:user, login: "user") allow(user).to receive(:ldap_auth_source).and_return(auth_source) user end context "and successful external authentication" do before do expect(auth_source).to receive(:authenticate).with("user", "password").and_return(true) end it "succeeds" do expect(described_class.try_authentication_for_existing_user(user_with_external_auth_source, "password")) .to eq(user_with_external_auth_source) end end context "and failing external authentication" do before do expect(auth_source).to receive(:authenticate).with("user", "password").and_return(false) end it "fails when the authentication fails" do expect(described_class.try_authentication_for_existing_user(user_with_external_auth_source, "password")) .to be_nil end end end end describe "#wants_comments_in_reverse_order?" do let(:user) { create(:user) } it "is false by default" do expect(user) .not_to be_wants_comments_in_reverse_order end it "is false if set to asc" do user.pref.comments_sorting = "asc" expect(user) .not_to be_wants_comments_in_reverse_order end it "is true if set to asc" do user.pref.comments_sorting = "desc" expect(user) .to be_wants_comments_in_reverse_order end end describe "#roles_for_project" do let(:project) { create(:project) } let!(:user) do create(:user, member_with_roles: { project => roles }) end let(:roles) { create_list(:project_role, 2) } context "for a project the user has roles in" do it "returns the roles" do expect(user.roles_for_project(project)) .to match_array roles end end context "for a project the user does not have roles in" do let(:other_project) { create(:project) } it "returns an empty set" do expect(user.roles_for_project(other_project)) .to be_empty end end end describe "#roles_for_work_package" do let(:work_package) { create(:work_package) } let!(:user) do create(:user, member_with_roles: { work_package.project => project_roles, work_package => work_package_roles }) end let(:project_roles) { create_list(:project_role, 2) } let(:work_package_roles) { create_list(:work_package_role, 1) } context "for a work_package the user has roles in" do it "returns the roles" do expect(user.roles_for_work_package(work_package)) .to match_array project_roles + work_package_roles end end context "for a work_package the user does not have roles in" do let(:other_work_package) { create(:work_package) } it "returns an empty set" do expect(user.roles_for_work_package(other_work_package)) .to be_empty end end end describe ".system" do context "no SystemUser exists" do before do SystemUser.delete_all end it "creates a SystemUser" do expect do system_user = described_class.system expect(system_user).not_to be_new_record expect(system_user).to be_a(SystemUser) end.to change(described_class, :count).by(1) end end context "a SystemUser exists" do before do @u = described_class.system expect(SystemUser.first).to eq(@u) end it "returns existing SystemUser" do expect do system_user = described_class.system expect(system_user).to eq(@u) end.not_to change(described_class, :count) end end end describe ".default_admin_account_deleted_or_changed?" do let(:default_admin) do build(:user, login: "admin", password: "admin", password_confirmation: "admin", admin: true) end before do Setting.password_min_length = 5 end context "default admin account exists with default password" do before do default_admin.save end it { expect(described_class).not_to be_default_admin_account_changed } end context "default admin account exists with changed password" do before do default_admin.update_attribute :password, "dafaultAdminPwd" default_admin.update_attribute :password_confirmation, "dafaultAdminPwd" default_admin.save end it { expect(described_class).to be_default_admin_account_changed } end context "default admin account was deleted" do before do default_admin.save default_admin.delete end it { expect(described_class).to be_default_admin_account_changed } end context "default admin account was disabled" do before do default_admin.status = described_class.statuses[:locked] default_admin.save end it { expect(described_class).to be_default_admin_account_changed } end end describe ".find_by_rss_key" do let(:rss_key) { user.rss_key } context "feeds enabled" do before do allow(Setting).to receive(:feeds_enabled?).and_return(true) end it { expect(described_class.find_by_rss_key(rss_key)).to eq(user) } end context "feeds disabled" do before do allow(Setting).to receive(:feeds_enabled?).and_return(false) end it { expect(described_class.find_by_rss_key(rss_key)).to be_nil } end end describe "#rss_key" do let(:user) { create(:user) } it "is created on the fly" do expect { user.rss_key } .to change { user.reload.rss_token.nil? } .from(true) .to(false) end it "is persisted" do key = user.rss_key expect(user.reload.rss_key) .to eq key end it "has a length of 64" do expect(user.rss_key.length) .to eq 64 end end describe "#ical_tokens" do let(:user) { create(:user) } let(:query) { create(:query, user:) } let(:ical_token) { create(:ical_token, user:, query:, name: "My Token") } let(:another_ical_token) { create(:ical_token, user:, query:, name: "My Other Token") } it "are not present by default" do expect(user.ical_tokens) .to be_empty end it "returns all existing ical tokens from this user" do ical_token another_ical_token expect(user.ical_tokens).to contain_exactly(ical_token, another_ical_token) end it "are destroyed when the user is destroyed" do ical_token another_ical_token user.destroy expect(Token::ICal.all).to be_empty end end describe ".newest" do let!(:anonymous) { described_class.anonymous } let!(:user1) { create(:user) } let!(:user2) { create(:user) } let(:newest) { described_class.newest.to_a } it "without anonymous user", :aggregate_failures do expect(newest).to include(user1) expect(newest).to include(user2) expect(newest).not_to include(anonymous) end end describe "#mail_regexp" do it "handles suffixed mails" do _, suffixed = described_class.mail_regexp("foo+bar@example.org") expect(suffixed).to be_truthy end end describe "#time_zone" do let(:user) { build(:user, preferences:) } context "with an existing time zone in the prefs" do let(:preferences) { { "time_zone" => "Europe/Athens" } } it "returns the matching ActiveSupport::TimeZone" do expect(user.time_zone) .to eql ActiveSupport::TimeZone["Europe/Athens"] end end context "with an invalid time zone" do # Would need to be Etc/UTC or UTC to be valid let(:preferences) { { "time_zone" => "utc" } } it "returns the utc ActiveSupport::TimeZone" do expect(user.time_zone) .to eql ActiveSupport::TimeZone["Etc/UTC"] end end context "without a time zone" do # Would need to be Etc/UTC or UTC to be valid let(:preferences) { {} } it "returns the utc ActiveSupport::TimeZone" do expect(user.time_zone) .to eql ActiveSupport::TimeZone["Etc/UTC"] end end end describe "#find_by_mail" do let!(:user1) { create(:user, mail: "foo+test@example.org") } let!(:user2) { create(:user, mail: "foo@example.org") } let!(:user3) { create(:user, mail: "foo-bar@example.org") } context "with default plus suffix" do it "finds users matching the suffix" do expect(Setting.mail_suffix_separators).to eq "+" # Can match either of the first two match2 = described_class.find_by_mail("foo@example.org") expect([user1.id, user2.id]).to include(match2.id) matches = described_class.where_mail_with_suffix("foo@example.org") expect(matches.pluck(:id)).to contain_exactly(user1.id, user2.id) matches = described_class.where_mail_with_suffix("foo+test@example.org") expect(matches.pluck(:id)).to contain_exactly(user1.id) end end context "with plus and minus suffix", with_settings: { mail_suffix_separators: "+-" } do it "finds users matching the suffix" do expect(Setting.mail_suffix_separators).to eq "+-" match1 = described_class.find_by_mail("foo-bar@example.org") expect(match1).to eq(user3) # Can match either of the three match2 = described_class.find_by_mail("foo@example.org") expect([user1.id, user2.id, user3.id]).to include(match2.id) matches = described_class.where_mail_with_suffix("foo@example.org") expect(matches.pluck(:id)).to contain_exactly(user1.id, user2.id, user3.id) end end end describe ".try_to_login" do let(:password) { "pwd123Password!" } let(:login) { "the_login" } let(:status) { described_class.statuses[:active] } let!(:user) do create(:user, password:, password_confirmation: password, login:, status:) end context "with good credentials" do it "returns the user" do expect(described_class.try_to_login(login, password)) .to eq user end end context "with wrong password" do it "returns the user" do expect(described_class.try_to_login(login, "#{password}!")) .to be_nil end end context "with wrong case in login" do it "returns the user" do expect(described_class.try_to_login("The_login", password)) .to eq user end end context "with wrong characters in login" do it "returns nil" do expect(described_class.try_to_login(login[0..-2], password)) .to be_nil end end context "with the user being locked" do let(:status) { described_class.statuses[:locked] } it "returns nil" do expect(described_class.try_to_login(login, "#{password}!")) .to be_nil end end context "with the user's password being changed" do let(:new_password) { "newPWD12%abc" } before do user.password = new_password user.save! end it "returns the user" do expect(described_class.try_to_login(login, new_password)) .to eq user end end end describe ".find_by_api_key" do let(:status) { described_class.statuses[:active] } let!(:user) do create(:user, status:) end let!(:token) do create(:api_token, user:) end context "if the right token is used" do it "returns the user" do expect(described_class.find_by_api_key(token.plain_value)) .to eq user end end context "if it isn't the right user" do it "returns nil" do expect(described_class.find_by_api_key("#{token.value}abc")) .to be_nil end end context "if the right token is used but the user is locked" do let(:status) { described_class.statuses[:locked] } it "returns nil" do expect(described_class.find_by_api_key(token.plain_value)) .to be_nil end end end describe ".find_by_mail" do let(:mail) { "the@mail.org" } let!(:user) { create(:user, mail:) } context "with the exact mail" do it "finds the user" do expect(described_class.find_by(mail:)) .to eq user end end context "with the mail address in uppercase" do it "finds the user" do expect(described_class.find_by_mail(mail.upcase)) .to eq user end end context "with a different mail address" do it "is nil" do expect(described_class.find_by_mail(mail[1..-2])) .to be_nil end end context "with a mail suffix in the address" do let(:mail) { "the+other@mail.org" } it "finds the user" do expect(described_class.find_by_mail("the@mail.org")) .to eq user end end end describe ".anonymous" do it "creates an anonymous user on the fly" do expect(described_class.anonymous) .to be_a(AnonymousUser) end it "creates a persisted record" do expect(described_class.anonymous) .to be_persisted end end it_behaves_like "creates an audit trail on destroy" do subject { create(:attachment) } end it_behaves_like "acts_as_customizable included", admin_only_allowed: true, comments: false do let!(:model_instance) { create(:user) } let!(:new_model_instance) { user } let!(:custom_field) { create(:user_custom_field, :string) } end describe ".available_custom_fields" do let(:admin) { build_stubbed(:admin) } let(:user) { build_stubbed(:user) } shared_let(:user_cf) { create(:user_custom_field) } shared_let(:admin_user_cf) { create(:user_custom_field, admin_only: true) } context "for an admin" do current_user { admin } it "returns all fields including admin-only" do expect(user.available_custom_fields) .to contain_exactly(user_cf, admin_user_cf) end end context "for a member" do current_user { user } it "does not return admin-only field" do expect(user.available_custom_fields) .to contain_exactly(user_cf) end end end describe "#non_working_time_entities_for_year and #non_working_days_for_year" do let(:user) { create(:user) } let(:other_user) { create(:user) } let(:year) { 2025 } let!(:system_nwd) { create(:non_working_day, date: Date.new(year, 12, 25)) } let!(:user_nwd) { create(:user_non_working_time, user:, start_date: Date.new(year, 6, 16)) } let!(:other_user_nwd) { create(:user_non_working_time, user: other_user, start_date: Date.new(year, 7, 4)) } let!(:other_year_system_nwd) { create(:non_working_day, date: Date.new(year - 1, 12, 25)) } let!(:other_year_user_nwd) { create(:user_non_working_time, user:, start_date: Date.new(year - 1, 6, 16)) } describe "#non_working_days_for_year" do subject { user.non_working_days_for_year(year) } it "includes system-wide non-working days" do expect(subject).to include(system_nwd.date) end it "includes the user's own non-working days" do expect(subject).to include(user_nwd.start_date) end it "does not include other users' non-working days" do expect(subject).not_to include(other_user_nwd.start_date) end it "does not include dates from other years" do expect(subject).not_to include(other_year_system_nwd.date, other_year_user_nwd.start_date) end context "when the user non-working time spans multiple days" do # July 7–13, 2025 is a Monday–Sunday; does not overlap with outer user_nwd (June 16) let!(:week_nwd) do create(:user_non_working_time, user:, start_date: Date.new(year, 7, 7), end_date: Date.new(year, 7, 13)) end it "expands the range into individual working days" do expect(subject).to include(Date.new(year, 7, 7), Date.new(year, 7, 8), Date.new(year, 7, 9), Date.new(year, 7, 10), Date.new(year, 7, 11)) end it "does not include weekend days within the range" do week_with_saturday_and_sunday_as_weekend expect(subject).not_to include(Date.new(year, 7, 12), Date.new(year, 7, 13)) end end context "when a user non-working day coincides with a system non-working day" do let!(:duplicate_user_nwd) { create(:user_non_working_time, user:, start_date: system_nwd.date) } it "returns the date only once" do expect(subject.count { |d| d == system_nwd.date }).to eq(1) end end end describe "#non_working_time_entities_for_year" do subject { user.non_working_time_entities_for_year(year) } it "returns NonWorkingDay and UserNonWorkingTime records" do expect(subject).to include(system_nwd, user_nwd) end it "does not include other users' non-working days" do expect(subject).not_to include(other_user_nwd) end it "does not include records from other years" do expect(subject).not_to include(other_year_system_nwd, other_year_user_nwd) end end end end