Validate special chars when creating a team (#209)

* Add validation for team account names

- Prevent creating teams with reserved names like 'billing' and 'settings'
- Add regex validation to block team names with special characters
- Update localization for new error messages
- Extend E2E tests to cover various invalid team name scenarios

* Enhance team account name validation and slug generation

- Add comprehensive tests for account slug generation in Supabase
- Improve team name validation schema to handle special characters
- Add form validation message display in update team account name form
- Refine slug generation to handle various edge cases like special characters, non-ASCII text, and mixed case
This commit is contained in:
Giancarlo Buomprisco
2025-03-11 09:58:21 +07:00
committed by GitHub
parent b265f596da
commit bd723dccce
6 changed files with 205 additions and 13 deletions

View File

@@ -16,7 +16,7 @@ export class TeamAccountsPageObject {
async setup(params = this.createTeamName()) { async setup(params = this.createTeamName()) {
const { email } = await this.auth.signUpFlow('/home'); const { email } = await this.auth.signUpFlow('/home');
await this.createTeam(params); await this.createTeam(params);
return { email, teamName: params.teamName, slug: params.slug }; return { email, teamName: params.teamName, slug: params.slug };
@@ -78,6 +78,16 @@ export class TeamAccountsPageObject {
}).toPass(); }).toPass();
} }
async tryCreateTeam(teamName: string) {
await this.page.locator('[data-test="create-team-form"] input').fill('');
await this.page.waitForTimeout(200);
await this.page.locator('[data-test="create-team-form"] input').fill(teamName);
return this.page.click(
'[data-test="create-team-form"] button:last-child',
);
}
async createTeam({ teamName, slug } = this.createTeamName()) { async createTeam({ teamName, slug } = this.createTeamName()) {
await this.openAccountsSelector(); await this.openAccountsSelector();
@@ -132,20 +142,20 @@ export class TeamAccountsPageObject {
// Find the member row and click the actions button // Find the member row and click the actions button
const memberRow = this.page.getByRole('row', { name: memberEmail }); const memberRow = this.page.getByRole('row', { name: memberEmail });
await memberRow.getByRole('button').click(); await memberRow.getByRole('button').click();
// Click the update role option in the dropdown menu // Click the update role option in the dropdown menu
await this.page.getByText('Update Role').click(); await this.page.getByText('Update Role').click();
// Select the new role // Select the new role
await this.page.click('[data-test="role-selector-trigger"]'); await this.page.click('[data-test="role-selector-trigger"]');
await this.page.click(`[data-test="role-option-${newRole}"]`); await this.page.click(`[data-test="role-option-${newRole}"]`);
// Click the confirm button // Click the confirm button
const click = this.page.click('[data-test="confirm-update-member-role"]'); const click = this.page.click('[data-test="confirm-update-member-role"]');
// Wait for the update to complete and page to reload // Wait for the update to complete and page to reload
const response = this.page.waitForURL('**/home/*/members'); const response = this.page.waitForURL('**/home/*/members');
return Promise.all([click, response]); return Promise.all([click, response]);
}).toPass(); }).toPass();
} }
@@ -155,19 +165,21 @@ export class TeamAccountsPageObject {
// Find the member row and click the actions button // Find the member row and click the actions button
const memberRow = this.page.getByRole('row', { name: memberEmail }); const memberRow = this.page.getByRole('row', { name: memberEmail });
await memberRow.getByRole('button').click(); await memberRow.getByRole('button').click();
// Click the transfer ownership option in the dropdown menu // Click the transfer ownership option in the dropdown menu
await this.page.getByText('Transfer Ownership').click(); await this.page.getByText('Transfer Ownership').click();
// Complete OTP verification // Complete OTP verification
await this.otp.completeOtpVerification(ownerEmail); await this.otp.completeOtpVerification(ownerEmail);
// Click the confirm button // Click the confirm button
const click = this.page.click('[data-test="confirm-transfer-ownership-button"]'); const click = this.page.click(
'[data-test="confirm-transfer-ownership-button"]',
);
// Wait for the transfer to complete and page to reload // Wait for the transfer to complete and page to reload
const response = this.page.waitForURL('**/home/*/members'); const response = this.page.waitForURL('**/home/*/members');
return Promise.all([click, response]); return Promise.all([click, response]);
}).toPass(); }).toPass();
} }

View File

@@ -96,6 +96,99 @@ test.describe('Team Accounts', () => {
await expect(teamAccounts.getTeamFromSelector(teamName)).toBeVisible(); await expect(teamAccounts.getTeamFromSelector(teamName)).toBeVisible();
}); });
test('cannot create a Team account using reserved names', async ({
page,
}) => {
const teamAccounts = new TeamAccountsPageObject(page);
await teamAccounts.setup();
await teamAccounts.openAccountsSelector();
await page.click('[data-test="create-team-account-trigger"]');
await teamAccounts.tryCreateTeam('billing');
await expect(
page.getByText('This name is reserved. Please choose a different one.'),
).toBeVisible();
await teamAccounts.tryCreateTeam('settings');
await expect(
page.getByText('This name is reserved. Please choose a different one.'),
).toBeVisible();
function expectError() {
return expect(
page.getByText(
'This name cannot contain special characters. Please choose a different one.',
),
).toBeVisible();
}
await teamAccounts.tryCreateTeam('Test-Name#');
await expectError();
await teamAccounts.tryCreateTeam('Test,Name');
await expectError();
await teamAccounts.tryCreateTeam('Test Name/')
await expectError();
await teamAccounts.tryCreateTeam('Test Name\\')
await expectError();
await teamAccounts.tryCreateTeam('Test Name:')
await expectError();
await teamAccounts.tryCreateTeam('Test Name;')
await expectError();
await teamAccounts.tryCreateTeam('Test Name=');
await expectError();
await teamAccounts.tryCreateTeam('Test Name>');
await expectError();
await teamAccounts.tryCreateTeam('Test Name<');
await expectError();
await teamAccounts.tryCreateTeam('Test Name?');
await expectError();
await teamAccounts.tryCreateTeam('Test Name@');
await expectError();
await teamAccounts.tryCreateTeam('Test Name^');
await expectError();
await teamAccounts.tryCreateTeam('Test Name&');
await expectError();
await teamAccounts.tryCreateTeam('Test Name*');
await expectError();
await teamAccounts.tryCreateTeam('Test Name(');
await expectError();
await teamAccounts.tryCreateTeam('Test Name)');
await expectError();
await teamAccounts.tryCreateTeam('Test Name+');
await expectError();
await teamAccounts.tryCreateTeam('Test Name%');
await expectError();
await teamAccounts.tryCreateTeam('Test Name$');
await expectError();
await teamAccounts.tryCreateTeam('Test Name[');
await expectError();
await teamAccounts.tryCreateTeam('Test Name]');
await expectError();
});
}); });
test.describe('Team Account Deletion', () => { test.describe('Team Account Deletion', () => {

View File

@@ -158,5 +158,6 @@
"joiningTeam": "Joining team...", "joiningTeam": "Joining team...",
"leaveTeamInputLabel": "Please type LEAVE to confirm leaving the team.", "leaveTeamInputLabel": "Please type LEAVE to confirm leaving the team.",
"leaveTeamInputDescription": "By leaving the team, you will no longer have access to it.", "leaveTeamInputDescription": "By leaving the team, you will no longer have access to it.",
"reservedNameError": "This name is reserved. Please choose a different one." "reservedNameError": "This name is reserved. Please choose a different one.",
"specialCharactersError": "This name cannot contain special characters. Please choose a different one."
} }

View File

@@ -51,6 +51,78 @@ select throws_ok(
'duplicate key value violates unique constraint "accounts_slug_key"' 'duplicate key value violates unique constraint "accounts_slug_key"'
); );
-- Test special characters in the slug
update public.accounts set slug = 'test-5' where slug = 'test-4[';
select row_eq(
$$ select slug from public.accounts where name = 'Test 4' $$,
row('test-4'::text),
'Updating the name of a team account should update the slug'
);
-- Test various special characters
update public.accounts set name = 'Test@Special#Chars$' where slug = 'test-4';
select row_eq(
$$ select slug from public.accounts where name = 'Test@Special#Chars$' $$,
row('test-special-chars'::text),
'Special characters should be removed from slug'
);
-- Test multiple consecutive special characters
update public.accounts set name = 'Test!!Multiple---Special$$$Chars' where slug = 'test-special-chars';
select row_eq(
$a$ select slug from public.accounts where name = 'Test!!Multiple---Special$$$Chars' $a$,
row('test-multiple-special-chars'::text),
'Multiple consecutive special characters should be replaced with single hyphen'
);
-- Test leading and trailing special characters
update public.accounts set name = '!!!LeadingAndTrailing###' where slug = 'test-multiple-special-chars';
select row_eq(
$$ select slug from public.accounts where name = '!!!LeadingAndTrailing###' $$,
row('leadingandtrailing'::text),
'Leading and trailing special characters should be removed'
);
-- Test non-ASCII characters
update public.accounts set name = 'Testéñ中文Русский' where slug = 'leadingandtrailing';
select row_eq(
$$ select slug from public.accounts where name = 'Testéñ中文Русский' $$,
row('testen'::text),
'Non-ASCII characters should be transliterated or removed'
);
-- Test mixed case with special characters
update public.accounts set name = 'Test Mixed CASE With Special@Chars!' where slug = 'testen';
select row_eq(
$$ select slug from public.accounts where name = 'Test Mixed CASE With Special@Chars!' $$,
row('test-mixed-case-with-special-chars'::text),
'Mixed case should be converted to lowercase and special chars handled'
);
-- Test using parentheses
update public.accounts set name = 'Test (Parentheses)' where slug = 'test-mixed-case-with-special-chars';
select row_eq(
$$ select slug from public.accounts where name = 'Test (Parentheses)' $$,
row('test-parentheses'::text),
'Parentheses should be removed from slug'
);
-- Test using asterisk
update public.accounts set name = 'Test * Asterisk' where slug = 'test-parentheses';
select row_eq(
$$ select slug from public.accounts where name = 'Test * Asterisk' $$,
row('test-asterisk'::text),
'Asterisk should be removed from slug'
);
select * from finish(); select * from finish();
ROLLBACK; ROLLBACK;

View File

@@ -16,6 +16,7 @@ import {
FormField, FormField,
FormItem, FormItem,
FormLabel, FormLabel,
FormMessage,
} from '@kit/ui/form'; } from '@kit/ui/form';
import { Input } from '@kit/ui/input'; import { Input } from '@kit/ui/input';
import { Trans } from '@kit/ui/trans'; import { Trans } from '@kit/ui/trans';
@@ -98,6 +99,8 @@ export const UpdateTeamAccountNameForm = (props: {
{...field} {...field}
/> />
</FormControl> </FormControl>
<FormMessage />
</FormItem> </FormItem>
); );
}} }}

View File

@@ -12,6 +12,8 @@ const RESERVED_NAMES_ARRAY = [
// please add more reserved names here // please add more reserved names here
]; ];
const SPECIAL_CHARACTERS_REGEX = /[!@#$%^&*()+=[\]{};':"\\|,.<>/?]/;
/** /**
* @name TeamNameSchema * @name TeamNameSchema
*/ */
@@ -21,6 +23,15 @@ export const TeamNameSchema = z
}) })
.min(2) .min(2)
.max(50) .max(50)
.refine(
(name) => {
console.log(name);
return !SPECIAL_CHARACTERS_REGEX.test(name);
},
{
message: 'teams:specialCharactersError',
},
)
.refine( .refine(
(name) => { (name) => {
return !RESERVED_NAMES_ARRAY.includes(name.toLowerCase()); return !RESERVED_NAMES_ARRAY.includes(name.toLowerCase());