Files
leadchat/spec/models/user_spec.rb
Muhsin Keloth 7554156abe chore: Account switching issue in newly added accounts (#12403)
The system determines a user’s active account by checking the
`active_at` field in the `account_users` table and selecting the most
recently active account:

```ruby
def active_account_user
  account_users.order(active_at: :desc)&.first
end
```

This works fine when all accounts have a valid active_at timestamp.

**Problem**

When a user is added to a new account, the `active_at` value is NULL
(because the account has never been explicitly activated). Ordering by
active_at DESC produces inconsistent results across databases, since
handling of NULL values differs (sometimes treated as high, sometimes
low).

As a result:

- Mobile apps (critical impact): `/profile` returns the wrong account.
The UI keeps showing the old account even after switching, and
restarting does not fix it.
- Web app (accidentally works): Appears correct because the active
account is inferred from the browser URL, but the backend API is still
wrong.

**Root Cause**

- The ordering logic did not account for NULL `active_at`.
- New accounts without active_at sometimes get incorrectly prioritized
as the “active” account.

**Solution**

Explicitly ensure that accounts with NULL active_at are sorted after
accounts with real timestamps by using NULLS LAST:

```ruby
def active_account_user
  account_users.order(Arel.sql('active_at DESC NULLS LAST, id DESC'))&.first
end
```

- Accounts with actual `active_at` values will always be prioritized.
- New accounts (with NULL active_at) will be placed at the bottom until
the user explicitly activates them.
- Adding id DESC as a secondary ordering ensures consistent tie-breaking
when multiple accounts have the same `active_at`.
2025-09-10 14:12:22 +05:30

151 lines
5.2 KiB
Ruby

# frozen_string_literal: true
require 'rails_helper'
require Rails.root.join 'spec/models/concerns/access_tokenable_shared.rb'
require Rails.root.join 'spec/models/concerns/avatarable_shared.rb'
RSpec.describe User do
let!(:user) { create(:user) }
context 'with validations' do
it { is_expected.to validate_presence_of(:email) }
end
context 'with associations' do
it { is_expected.to have_many(:accounts).through(:account_users) }
it { is_expected.to have_many(:account_users) }
it { is_expected.to have_many(:assigned_conversations).class_name('Conversation').dependent(:nullify) }
it { is_expected.to have_many(:inbox_members).dependent(:destroy_async) }
it { is_expected.to have_many(:notification_settings).dependent(:destroy_async) }
it { is_expected.to have_many(:messages) }
it { is_expected.to have_many(:reporting_events) }
it { is_expected.to have_many(:teams) }
end
describe 'concerns' do
it_behaves_like 'access_tokenable'
it_behaves_like 'avatarable'
end
describe 'pubsub_token' do
before { user.update(name: Faker::Name.name) }
it { expect(user.pubsub_token).not_to be_nil }
it { expect(user.saved_changes.keys).not_to eq('pubsub_token') }
context 'with rotate the pubsub_token' do
it 'changes the pubsub_token when password changes' do
pubsub_token = user.pubsub_token
user.password = Faker::Internet.password(special_characters: true)
user.save!
expect(user.pubsub_token).not_to eq(pubsub_token)
end
it 'will not change pubsub_token when other attributes change' do
pubsub_token = user.pubsub_token
user.name = Faker::Name.name
user.save!
expect(user.pubsub_token).to eq(pubsub_token)
end
end
end
describe 'hmac_identifier' do
it 'return nil if CHATWOOT_INBOX_HMAC_KEY is not set' do
expect(user.hmac_identifier).to eq('')
end
it 'return value if CHATWOOT_INBOX_HMAC_KEY is set' do
ConfigLoader.new.process
i = InstallationConfig.find_by(name: 'CHATWOOT_INBOX_HMAC_KEY')
i.value = 'random_secret_key'
i.save!
GlobalConfig.clear_cache
expected_hmac_identifier = OpenSSL::HMAC.hexdigest('sha256', 'random_secret_key', user.email)
expect(user.hmac_identifier).to eq expected_hmac_identifier
end
end
context 'with sso_auth_token' do
it 'can generate multiple sso tokens which can be validated' do
sso_auth_token1 = user.generate_sso_auth_token
sso_auth_token2 = user.generate_sso_auth_token
expect(sso_auth_token1).present?
expect(sso_auth_token2).present?
expect(user.valid_sso_auth_token?(sso_auth_token1)).to be true
expect(user.valid_sso_auth_token?(sso_auth_token2)).to be true
end
it 'wont validate an invalid token' do
expect(user.valid_sso_auth_token?(SecureRandom.hex(32))).to be false
end
it 'wont validate an invalidated token' do
sso_auth_token = user.generate_sso_auth_token
user.invalidate_sso_auth_token(sso_auth_token)
expect(user.valid_sso_auth_token?(sso_auth_token)).to be false
end
end
describe 'access token' do
it 'creates a single access token upon user creation' do
new_user = create(:user)
token_count = AccessToken.where(owner: new_user).count
expect(token_count).to eq(1)
end
end
context 'when user changes the email' do
it 'mutates the value' do
user.email = 'user@example.com'
expect(user.will_save_change_to_email?).to be true
end
end
context 'when the supplied email is uppercase' do
it 'downcases the email on save' do
new_user = create(:user, email: 'Test123@test.com')
expect(new_user.email).to eq('test123@test.com')
end
end
describe '#active_account_user' do
let(:user) { create(:user) }
let(:account1) { create(:account) }
let(:account2) { create(:account) }
let(:account3) { create(:account) }
before do
# Create account_users with different active_at values
create(:account_user, user: user, account: account1, active_at: 2.days.ago)
create(:account_user, user: user, account: account2, active_at: 1.day.ago)
create(:account_user, user: user, account: account3, active_at: nil) # New account with NULL active_at
end
it 'returns the account_user with the most recent active_at, prioritizing timestamps over NULL values' do
# Should return account2 (most recent timestamp) even though account3 was created last with NULL active_at
expect(user.active_account_user.account_id).to eq(account2.id)
end
it 'returns NULL active_at account only when no other accounts have active_at' do
# Remove active_at from all accounts
user.account_users.each { |au| au.update!(active_at: nil) }
# Should return one of the accounts (behavior is undefined but consistent)
expect(user.active_account_user).to be_present
end
context 'when multiple accounts have NULL active_at' do
before do
create(:account_user, user: user, account: create(:account), active_at: nil)
end
it 'still prioritizes accounts with timestamps' do
expect(user.active_account_user.account_id).to eq(account2.id)
end
end
end
end