Team names created via the API could contain control characters (for
example a trailing newline). Because the team-delete confirmation dialog
requires you to retype the team name and matches it against the stored
value, a hidden control character meant the typed name never matched —
leaving the team impossible to delete from the UI. This sanitizes team
names on save so they stay clean and deletable.
#### How to reproduce
1. Create a team via `POST /api/v1/accounts/{account_id}/teams` with
`{"name": "test\n"}`.
2. The team is created with the trailing newline stored in `name`.
3. In **Settings → Teams**, click delete and type the team name to
confirm — the match fails, so the team cannot be deleted.
#### What changed
- `app/models/team.rb`: the existing `before_validation` now strips
control characters and surrounding whitespace before downcasing the
name. Names that reduce to blank (e.g. only newlines/tabs) are rejected
loudly by the existing `presence` validation.
- Fixing at the model layer covers the API and every other create/update
path, rather than relying on the frontend confirm-dialog `.trim()`
(which only handles leading/trailing whitespace, not internal control
characters).
Note: this prevents new malformed names. Any team already saved with a
control character can be made deletable again simply by renaming it (an
update re-runs the same sanitization).
| Input | Stored as | Result |
|---|---|---|
| `"test\n"` | `"test"` | valid, deletable |
| `"te\nst"` (internal) | `"test"` | valid |
| `"\t\n "` (only control/ws) | — | rejected: "Name must not be blank" |
| `"Customer Support"` | `"customer support"` | unchanged behavior |
🤖 Generated with [Claude Code](https://claude.com/claude-code)
---------
Co-authored-by: Muhsin <12408980+muhsin-k@users.noreply.github.com>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
82 lines
3.2 KiB
Ruby
82 lines
3.2 KiB
Ruby
require 'rails_helper'
|
|
|
|
RSpec.describe Team do
|
|
describe 'associations' do
|
|
it { is_expected.to belong_to(:account) }
|
|
it { is_expected.to have_many(:conversations) }
|
|
it { is_expected.to have_many(:team_members) }
|
|
end
|
|
|
|
describe 'name normalization' do
|
|
let(:account) { create(:account) }
|
|
|
|
it 'downcases the name' do
|
|
team = create(:team, account: account, name: 'Customer Support')
|
|
expect(team.name).to eq('customer support')
|
|
end
|
|
|
|
it 'strips control characters and surrounding whitespace' do
|
|
team = create(:team, account: account, name: " Sales\n")
|
|
expect(team.name).to eq('sales')
|
|
end
|
|
|
|
it 'removes control characters embedded within the name' do
|
|
team = create(:team, account: account, name: "su\npport")
|
|
expect(team.name).to eq('support')
|
|
end
|
|
|
|
it 'is invalid when the name reduces to blank after sanitization' do
|
|
team = build(:team, account: account, name: "\t\n ")
|
|
expect(team).not_to be_valid
|
|
expect(team.errors[:name]).to include(I18n.t('errors.validations.presence'))
|
|
end
|
|
end
|
|
|
|
describe '#add_members' do
|
|
let(:team) { FactoryBot.create(:team) }
|
|
|
|
before do
|
|
allow(Rails.configuration.dispatcher).to receive(:dispatch)
|
|
end
|
|
|
|
it 'handles adds all members and resets cache keys' do
|
|
users = FactoryBot.create_list(:user, 3)
|
|
team.add_members(users.map(&:id))
|
|
expect(team.reload.team_members.size).to eq(3)
|
|
|
|
expect(Rails.configuration.dispatcher).to have_received(:dispatch).at_least(:once)
|
|
.with(
|
|
'account.cache_invalidated',
|
|
kind_of(Time),
|
|
account: team.account,
|
|
cache_keys: team.account.cache_keys
|
|
)
|
|
end
|
|
end
|
|
|
|
describe '#remove_members' do
|
|
let(:team) { FactoryBot.create(:team) }
|
|
let(:users) { FactoryBot.create_list(:user, 3) }
|
|
|
|
before do
|
|
team.add_members(users.map(&:id))
|
|
allow(Rails.configuration.dispatcher).to receive(:dispatch)
|
|
end
|
|
|
|
it 'removes the members and resets cache keys' do
|
|
expect(team.reload.team_members.size).to eq(3)
|
|
|
|
team.remove_members(users.map(&:id))
|
|
expect(team.reload.team_members.size).to eq(0)
|
|
|
|
expect(Rails.configuration.dispatcher).to have_received(:dispatch).at_least(:once)
|
|
.with(
|
|
'account.cache_invalidated',
|
|
kind_of(Time),
|
|
account: team.account,
|
|
cache_keys: team.account.cache_keys
|
|
)
|
|
end
|
|
end
|
|
end
|