Files
owncast/web/tests/displayNameValidation.test.ts
Gabe Kangas 8eb8bdc18b Stop using empty/whitespace as chat display names (#4553)
* fix(chat): fixes #4522 to stop people from setting invalid display names

* fix(chat): also guard against non-ascii whitespace like non breaking space

* Update web/utils/displayNameValidation.ts

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* fix: handle additional whitespace

* Update web/utils/displayNameValidation.ts

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* Javascript formatting autofixes

* fix: deduplicate running of validation

* fix: fix error with useMemo

* Update web/utils/displayNameValidation.ts

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* Update web/utils/displayNameValidation.ts

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* Javascript formatting autofixes

* fix: fix component rendering issue

---------

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Owncast <owncast@owncast.online>
2025-09-15 13:37:39 -07:00

191 lines
8.2 KiB
TypeScript

import { validateDisplayName, trimUnicodeWhitespace } from '../utils/displayNameValidation';
describe('Display Name Validation', () => {
const currentName = 'CurrentUser';
const characterLimit = 30;
describe('Valid names', () => {
test('should accept valid name', () => {
const result = validateDisplayName('NewUser', currentName, characterLimit);
expect(result.isValid).toBe(true);
expect(result.trimmedName).toBe('NewUser');
expect(result.errorMessage).toBeUndefined();
});
test('should trim whitespace and accept valid name', () => {
const result = validateDisplayName(' NewUser ', currentName, characterLimit);
expect(result.isValid).toBe(true);
expect(result.trimmedName).toBe('NewUser');
});
test('should trim Unicode whitespace and accept valid name', () => {
const result = validateDisplayName(
'\u00A0\u2000 NewUser \u2001\u00A0',
currentName,
characterLimit,
);
expect(result.isValid).toBe(true);
expect(result.trimmedName).toBe('NewUser');
});
test('should accept name with Unicode characters', () => {
const result = validateDisplayName('用户名', currentName, characterLimit);
expect(result.isValid).toBe(true);
expect(result.trimmedName).toBe('用户名');
});
test('should accept name at character limit', () => {
const longName = 'A'.repeat(characterLimit);
const result = validateDisplayName(longName, currentName, characterLimit);
expect(result.isValid).toBe(true);
expect(result.trimmedName).toBe(longName);
});
});
describe('Invalid names - whitespace and empty', () => {
test('should reject undefined name', () => {
const result = validateDisplayName(undefined, currentName, characterLimit);
expect(result.isValid).toBe(false);
expect(result.errorMessage).toBe('Display name is required');
});
test('should reject empty string', () => {
const result = validateDisplayName('', currentName, characterLimit);
expect(result.isValid).toBe(false);
expect(result.errorMessage).toBe('Display name cannot be empty or contain only whitespace');
});
test('should reject spaces only', () => {
const result = validateDisplayName(' ', currentName, characterLimit);
expect(result.isValid).toBe(false);
expect(result.errorMessage).toBe('Display name cannot be empty or contain only whitespace');
});
test('should reject tabs only', () => {
const result = validateDisplayName('\t\t\t', currentName, characterLimit);
expect(result.isValid).toBe(false);
expect(result.errorMessage).toBe('Display name cannot be empty or contain only whitespace');
});
test('should reject newlines only', () => {
const result = validateDisplayName('\n\n', currentName, characterLimit);
expect(result.isValid).toBe(false);
expect(result.errorMessage).toBe('Display name cannot be empty or contain only whitespace');
});
test('should reject mixed whitespace', () => {
const result = validateDisplayName(' \t\n\r ', currentName, characterLimit);
expect(result.isValid).toBe(false);
expect(result.errorMessage).toBe('Display name cannot be empty or contain only whitespace');
});
test('should reject Unicode whitespace', () => {
const result = validateDisplayName('\u00A0\u2000\u2001\u2002', currentName, characterLimit);
expect(result.isValid).toBe(false);
expect(result.errorMessage).toBe('Display name cannot be empty or contain only whitespace');
});
test('should reject mixed ASCII and Unicode whitespace', () => {
const result = validateDisplayName(' \t\u00A0\u2000\n\u2001 ', currentName, characterLimit);
expect(result.isValid).toBe(false);
expect(result.errorMessage).toBe('Display name cannot be empty or contain only whitespace');
});
test('should reject zero-width spaces and invisible characters', () => {
const result = validateDisplayName('\u200B\u200C\u200D\uFEFF', currentName, characterLimit);
expect(result.isValid).toBe(false);
expect(result.errorMessage).toBe('Display name cannot be empty or contain only whitespace');
});
});
describe('Invalid names - other reasons', () => {
test('should reject name same as current', () => {
const result = validateDisplayName(currentName, currentName, characterLimit);
expect(result.isValid).toBe(false);
expect(result.errorMessage).toBe('New name must be different from current name');
});
test('should reject name same as current after trimming', () => {
const result = validateDisplayName(` ${currentName} `, currentName, characterLimit);
expect(result.isValid).toBe(false);
expect(result.errorMessage).toBe('New name must be different from current name');
});
test('should reject name exceeding character limit', () => {
const longName = 'A'.repeat(characterLimit + 1);
const result = validateDisplayName(longName, currentName, characterLimit);
expect(result.isValid).toBe(false);
expect(result.errorMessage).toBe(`Display name cannot exceed ${characterLimit} characters`);
});
test('should handle Unicode characters correctly for length', () => {
// Emoji characters count as 2 code units but should be treated as 1 character
const emojiName = '😀'.repeat(characterLimit + 1);
const result = validateDisplayName(emojiName, currentName, characterLimit);
expect(result.isValid).toBe(false);
expect(result.errorMessage).toBe(`Display name cannot exceed ${characterLimit} characters`);
});
});
describe('Edge cases', () => {
test('should handle zero character limit', () => {
const result = validateDisplayName('A', currentName, 0);
expect(result.isValid).toBe(false);
expect(result.errorMessage).toBe('Display name cannot exceed 0 characters');
});
test('should handle very long current name', () => {
const veryLongCurrentName = 'A'.repeat(100);
const result = validateDisplayName('NewUser', veryLongCurrentName, characterLimit);
expect(result.isValid).toBe(true);
expect(result.trimmedName).toBe('NewUser');
});
test('should handle special characters', () => {
const specialName = '!@#$%^&*()_+{}|:"<>?[]\\;\',./-=`~';
const result = validateDisplayName(specialName, currentName, 50);
expect(result.isValid).toBe(true);
expect(result.trimmedName).toBe(specialName);
});
});
});
describe('Display Name Validation - Real-world test cases', () => {
const currentName = 'TestUser';
const testCases = [
{ input: 'John', expected: true, description: 'Simple name' },
{ input: 'John Doe', expected: true, description: 'Name with space' },
{ input: ' John ', expected: true, description: 'Name with padding spaces' },
{
input: '\u00A0\u2000 John \u2001\u00A0',
expected: true,
description: 'Name with Unicode whitespace padding',
},
{ input: '', expected: false, description: 'Empty string' },
{ input: ' ', expected: false, description: 'Only spaces' },
{ input: '\t', expected: false, description: 'Only tab' },
{ input: '\n', expected: false, description: 'Only newline' },
{ input: '\u00A0\u2000\u2001', expected: false, description: 'Only Unicode whitespace' },
{ input: 'TestUser', expected: false, description: 'Same as current' },
{ input: ' TestUser ', expected: false, description: 'Same as current with spaces' },
{
input: '\u00A0 TestUser \u2000',
expected: false,
description: 'Same as current with Unicode whitespace',
},
{ input: 'A'.repeat(31), expected: false, description: 'Too long (31 chars)' },
{ input: 'A'.repeat(30), expected: true, description: 'At limit (30 chars)' },
{ input: '用户', expected: true, description: 'Chinese characters' },
{ input: '😀🎉', expected: true, description: 'Emoji characters' },
];
test.each(testCases)('$description', ({ input, expected }) => {
const result = validateDisplayName(input, currentName, 30);
expect(result.isValid).toBe(expected);
if (expected && result.isValid) {
expect(result.trimmedName).toBe(trimUnicodeWhitespace(input));
}
});
});