mirror of
https://github.com/tiennm99/vngeoguessr.git
synced 2026-10-15 10:15:51 +00:00
Every Upstash command and Neon query now has a deadline (5 s and 8 s): the Redis client retried a hung connection past the browser's fifteen-second wait, and the Neon client had no limit. Usernames are NFC-normalised so a tone mark typed as a combining character is accepted and does not make two board members of one name. The debug key is compared in constant time. A panorama whose district the tree does not know fails the draw before a session is written, not /api/guess after it is consumed. The country branch of countPanos and the never-triggered per-city cap are gone.
58 lines
2.1 KiB
JavaScript
58 lines
2.1 KiB
JavaScript
import { describe, expect, it } from 'vitest';
|
|
import { generateRandomUsername, validateUsername } from '../src/lib/username.js';
|
|
|
|
// The generated name must satisfy the same rules UsernameModal enforces,
|
|
// because it is stored and later edited exactly like a typed name.
|
|
describe('generateRandomUsername', () => {
|
|
it('produces a name the modal validation accepts', () => {
|
|
for (let i = 0; i < 50; i += 1) {
|
|
const name = generateRandomUsername();
|
|
expect(name.length).toBeGreaterThanOrEqual(2);
|
|
expect(name.length).toBeLessThanOrEqual(20);
|
|
expect(name).toMatch(/^[a-zA-Z0-9_-]+$/);
|
|
expect(validateUsername(name).ok).toBe(true);
|
|
}
|
|
});
|
|
|
|
it('uses the Player- prefix so generated names are recognisable', () => {
|
|
expect(generateRandomUsername()).toMatch(/^Player-[0-9a-z]{6}$/);
|
|
});
|
|
});
|
|
|
|
// One rule for the modal and /api/guess: the browser used to be the only
|
|
// validator, so a request built by hand could put any string on the boards.
|
|
describe('validateUsername', () => {
|
|
it('trims and accepts an ordinary name', () => {
|
|
expect(validateUsername(' mai_99 ')).toEqual({ ok: true, value: 'mai_99', error: null });
|
|
});
|
|
|
|
it('accepts Vietnamese letters', () => {
|
|
expect(validateUsername('Tiến').ok).toBe(true);
|
|
expect(validateUsername('Nguyễn-Văn').ok).toBe(true);
|
|
});
|
|
|
|
it('composes a name typed with combining tone marks into the same member', () => {
|
|
// 'ả' as a plain 'a' followed by U+0309 HOOK ABOVE, which is what some
|
|
// Vietnamese keyboards and IMEs produce.
|
|
const decomposed = 'Tra\u0309n_ok';
|
|
const composed = 'Trản_ok';
|
|
expect(decomposed).not.toBe(composed);
|
|
expect(validateUsername(decomposed)).toEqual({ ok: true, value: composed, error: null });
|
|
});
|
|
|
|
it.each([
|
|
['not a string', 123],
|
|
['empty', ' '],
|
|
['too short', 'a'],
|
|
['too long', 'x'.repeat(21)],
|
|
['a space inside', 'mai le'],
|
|
// ':' is the separator inside distance-board members.
|
|
['a colon', 'mai:5'],
|
|
['punctuation', 'mai!'],
|
|
])('rejects %s', (_label, raw) => {
|
|
const result = validateUsername(raw);
|
|
expect(result.ok).toBe(false);
|
|
expect(result.error).toBeTruthy();
|
|
});
|
|
});
|