Stage 17: cap display-name special characters at 5 (ui + backend)
CI / changes (pull_request) Successful in 2s
CI / unit (pull_request) Successful in 8s
CI / integration (pull_request) Successful in 14s
CI / ui (pull_request) Successful in 35s
CI / gate (pull_request) Successful in 0s
CI / deploy (pull_request) Successful in 1m3s
CI / changes (pull_request) Successful in 2s
CI / unit (pull_request) Successful in 8s
CI / integration (pull_request) Successful in 14s
CI / ui (pull_request) Successful in 35s
CI / gate (pull_request) Successful in 0s
CI / deploy (pull_request) Successful in 1m3s
display_name validation gains a rule: at most 5 special characters — the '.' / '_' punctuation (spaces, which separate words, don't count) — so a still-well-formed name can't be mostly punctuation. Mirrored in the Go ValidateDisplayName and the UI validDisplayName; both unit-tested (5 ok, 6 rejected, 'J. R. R. Tolkien' ok). Docs: FUNCTIONAL (+ _ru).
This commit is contained in:
@@ -23,6 +23,11 @@ import (
|
|||||||
// is unbounded; auto-provisioned platform names bypass this editor validation).
|
// is unbounded; auto-provisioned platform names bypass this editor validation).
|
||||||
const maxDisplayName = 32
|
const maxDisplayName = 32
|
||||||
|
|
||||||
|
// maxDisplayNameSpecials caps the total special characters (the "." / "_" separators —
|
||||||
|
// every name rune that is neither a letter nor a space) an editable display name may
|
||||||
|
// carry, so a still-well-formed name cannot be made of mostly punctuation (Stage 17).
|
||||||
|
const maxDisplayNameSpecials = 5
|
||||||
|
|
||||||
// maxAwayWindow bounds the daily away window's duration (midnight-wrap aware).
|
// maxAwayWindow bounds the daily away window's duration (midnight-wrap aware).
|
||||||
const maxAwayWindow = 12 * time.Hour
|
const maxAwayWindow = 12 * time.Hour
|
||||||
|
|
||||||
@@ -110,6 +115,15 @@ func ValidateDisplayName(raw string) (string, error) {
|
|||||||
if !displayNameRe.MatchString(name) {
|
if !displayNameRe.MatchString(name) {
|
||||||
return "", fmt.Errorf("%w: display name has an invalid character or layout", ErrInvalidProfile)
|
return "", fmt.Errorf("%w: display name has an invalid character or layout", ErrInvalidProfile)
|
||||||
}
|
}
|
||||||
|
specials := 0
|
||||||
|
for _, r := range name {
|
||||||
|
if r != ' ' && !unicode.IsLetter(r) {
|
||||||
|
specials++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if specials > maxDisplayNameSpecials {
|
||||||
|
return "", fmt.Errorf("%w: display name has more than %d special characters", ErrInvalidProfile, maxDisplayNameSpecials)
|
||||||
|
}
|
||||||
return name, nil
|
return name, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -27,6 +27,9 @@ func TestValidateDisplayName(t *testing.T) {
|
|||||||
"digit rejected": {"Name2", "", false},
|
"digit rejected": {"Name2", "", false},
|
||||||
"blank": {" ", "", false},
|
"blank": {" ", "", false},
|
||||||
"too long": {strings.Repeat("a", 33), "", false},
|
"too long": {strings.Repeat("a", 33), "", false},
|
||||||
|
"five specials ok": {"a.a.a.a.a.a", "a.a.a.a.a.a", true}, // 5 dots
|
||||||
|
"six specials": {"a.a.a.a.a.a.a", "", false}, // 6 dots
|
||||||
|
"initials spaces ok": {"J. R. R. Tolkien", "J. R. R. Tolkien", true}, // 3 dots; spaces don't count
|
||||||
}
|
}
|
||||||
for name, tc := range cases {
|
for name, tc := range cases {
|
||||||
t.Run(name, func(t *testing.T) {
|
t.Run(name, func(t *testing.T) {
|
||||||
|
|||||||
+2
-1
@@ -141,7 +141,8 @@ new chat message raises an **unread badge** on the game's menu until the chat is
|
|||||||
|
|
||||||
### Profile & settings *(Stage 4 / 8)*
|
### Profile & settings *(Stage 4 / 8)*
|
||||||
Edit the display name (letters joined by a single space / "." / "_" separator, with an
|
Edit the display name (letters joined by a single space / "." / "_" separator, with an
|
||||||
optional trailing ".", up to 32 characters), the timezone (chosen as a UTC offset), the
|
optional trailing ".", up to 32 characters and at most 5 special characters — the "." / "_"
|
||||||
|
punctuation, spaces aside), the timezone (chosen as a UTC offset), the
|
||||||
daily away window (on a 10-minute grid, at most 12 hours, wrapping midnight) and the
|
daily away window (on a 10-minute grid, at most 12 hours, wrapping midnight) and the
|
||||||
block toggles. The profile form is edited inline (no separate edit mode). Linking
|
block toggles. The profile form is edited inline (no separate edit mode). Linking
|
||||||
an email or Telegram and merging accounts are covered under "Accounts, linking &
|
an email or Telegram and merging accounts are covered under "Accounts, linking &
|
||||||
|
|||||||
@@ -146,7 +146,8 @@ push доставляется через платформу.
|
|||||||
|
|
||||||
### Профиль и настройки *(Stage 4 / 8)*
|
### Профиль и настройки *(Stage 4 / 8)*
|
||||||
Редактирование отображаемого имени (буквы, разделённые одиночным пробелом / «.» /
|
Редактирование отображаемого имени (буквы, разделённые одиночным пробелом / «.» /
|
||||||
«_», с необязательной завершающей «.», до 32 символов), таймзоны (выбор смещения от
|
«_», с необязательной завершающей «.», до 32 символов и не более 5 спецсимволов —
|
||||||
|
пунктуации «.» / «_», пробелы не в счёт), таймзоны (выбор смещения от
|
||||||
UTC), суточного окна отсутствия (away; сетка по 10 минут, не более 12 часов, с
|
UTC), суточного окна отсутствия (away; сетка по 10 минут, не более 12 часов, с
|
||||||
переходом через полночь) и переключателей блокировок. Форма профиля редактируется
|
переходом через полночь) и переключателей блокировок. Форма профиля редактируется
|
||||||
сразу (без отдельного режима редактирования). Привязка email и Telegram, а также
|
сразу (без отдельного режима редактирования). Привязка email и Telegram, а также
|
||||||
|
|||||||
@@ -19,6 +19,9 @@ describe('validDisplayName', () => {
|
|||||||
['Name2', false],
|
['Name2', false],
|
||||||
['', false],
|
['', false],
|
||||||
['a'.repeat(33), false],
|
['a'.repeat(33), false],
|
||||||
|
['a.a.a.a.a.a', true], // 5 dots — at the special-char limit
|
||||||
|
['a.a.a.a.a.a.a', false], // 6 dots — over the limit
|
||||||
|
['J. R. R. Tolkien', true], // 3 dots; spaces are not special
|
||||||
])('%s -> %s', (name, ok) => {
|
])('%s -> %s', (name, ok) => {
|
||||||
expect(validDisplayName(name)).toBe(ok);
|
expect(validDisplayName(name)).toBe(ok);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -5,6 +5,10 @@
|
|||||||
/** maxDisplayName caps the editable display name in runes. */
|
/** maxDisplayName caps the editable display name in runes. */
|
||||||
export const maxDisplayName = 32;
|
export const maxDisplayName = 32;
|
||||||
|
|
||||||
|
/** maxDisplayNameSpecials caps the total special characters (the "." / "_" separators — every
|
||||||
|
* rune that is neither a letter nor a space) a display name may carry. Mirrors the Go rule. */
|
||||||
|
export const maxDisplayNameSpecials = 5;
|
||||||
|
|
||||||
/** maxAwayMinutes bounds the daily away window's length (12 h). */
|
/** maxAwayMinutes bounds the daily away window's length (12 h). */
|
||||||
export const maxAwayMinutes = 12 * 60;
|
export const maxAwayMinutes = 12 * 60;
|
||||||
|
|
||||||
@@ -17,7 +21,10 @@ const displayNameRe = /^\p{L}+(?:(?:[._] ?| )\p{L}+)*\.?$/u;
|
|||||||
/** displayNameError returns true when the trimmed name is a valid display name. */
|
/** displayNameError returns true when the trimmed name is a valid display name. */
|
||||||
export function validDisplayName(raw: string): boolean {
|
export function validDisplayName(raw: string): boolean {
|
||||||
const name = raw.trim();
|
const name = raw.trim();
|
||||||
return name.length > 0 && [...name].length <= maxDisplayName && displayNameRe.test(name);
|
const chars = [...name];
|
||||||
|
if (name.length === 0 || chars.length > maxDisplayName || !displayNameRe.test(name)) return false;
|
||||||
|
const specials = chars.filter((c) => c !== ' ' && !/\p{L}/u.test(c)).length;
|
||||||
|
return specials <= maxDisplayNameSpecials;
|
||||||
}
|
}
|
||||||
|
|
||||||
// A pragmatic email check (the backend re-validates with net/mail). Rejects spaces
|
// A pragmatic email check (the backend re-validates with net/mail). Rejects spaces
|
||||||
|
|||||||
Reference in New Issue
Block a user