fix: sanitize control characters in team names (#14865)

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>
This commit is contained in:
Muhsin Keloth
2026-07-07 18:04:24 -07:00
committed by GitHub
co-authored by Muhsin Claude Opus 4.8
parent 29d0b92f1c
commit ce8c8e9a11
2 changed files with 26 additions and 1 deletions
+1 -1
View File
@@ -30,7 +30,7 @@ class Team < ApplicationRecord
uniqueness: { scope: :account_id }
before_validation do
self.name = name.downcase if attribute_present?('name')
self.name = name.gsub(/[[:cntrl:]]/, '').strip.downcase if attribute_present?('name')
end
# Adds multiple members to the team
+25
View File
@@ -7,6 +7,31 @@ RSpec.describe Team do
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) }