- Widen users.role enum to include 'player'; make password_hash nullable; add email/email_verified/status/last_login_ip; pin username _ci collation. - POST /auth/register (honeypot + registerLimiter + botScore, reserved-name blocklist, duplicate->409, auto-login). player_registration setting gates it. - SSO auto-provision in finishLogin (setting-gated); return/portal-aware SSO redirects for the player portal; status refusal on login + requireAuth. - New /player self-service group (account, change username/password, TOTP, identities), reusing account.controller; accountChangeLimiter. - Admin: 'player' role + status/email on user create/update, role/status audit, player_registration enum validation, derived public registration flags. - usernamePolicy module (reserved, sanitize, derive, dedup) + unit tests; extend SSO callback tests. 133 server tests green. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019rao86n5cXpwAyjdBFEshV
53 lines
2.5 KiB
JavaScript
53 lines
2.5 KiB
JavaScript
// Unit tests for the pure username policy (no DB): validation, reserved-name
|
|
// blocklist, case normalization, SSO derivation + the dedup suffix loop.
|
|
const { test } = require('node:test')
|
|
const assert = require('node:assert/strict')
|
|
|
|
const policy = require('../src/auth/usernamePolicy')
|
|
|
|
test('validateUsername accepts a normal name and trims whitespace', () => {
|
|
const r = policy.validateUsername(' Frodo_99 ')
|
|
assert.equal(r.ok, true)
|
|
assert.equal(r.name, 'Frodo_99') // trimmed, case preserved
|
|
})
|
|
|
|
test('validateUsername rejects too-short / too-long / bad-charset names', () => {
|
|
assert.equal(policy.validateUsername('ab').ok, false) // < 3
|
|
assert.equal(policy.validateUsername('x'.repeat(33)).ok, false) // > 32
|
|
assert.equal(policy.validateUsername('has space').ok, false)
|
|
assert.equal(policy.validateUsername('emoji😀here').ok, false)
|
|
})
|
|
|
|
test('reserved names are rejected case-insensitively', () => {
|
|
for (const name of ['admin', 'ADMIN', 'Administrator', 'root', 'moderator', 'support', 'me']) {
|
|
assert.equal(policy.isReserved(name), true, `${name} should be reserved`)
|
|
assert.equal(policy.validateUsername(name).ok, false, `${name} should be rejected`)
|
|
}
|
|
assert.equal(policy.isReserved('frodo'), false)
|
|
})
|
|
|
|
test('sanitizeToUsername strips disallowed chars and leading punctuation', () => {
|
|
assert.equal(policy.sanitizeToUsername('Fró.do Baggins!'), 'Fro.doBaggins')
|
|
assert.equal(policy.sanitizeToUsername('...weird'), 'weird')
|
|
assert.equal(policy.sanitizeToUsername('a'.repeat(50)).length, policy.MAX_LEN)
|
|
})
|
|
|
|
test('deriveUsernameBase prefers display name, then email local-part, then player', () => {
|
|
assert.equal(policy.deriveUsernameBase({ name: 'Gandalf', email: 'g@x.com' }), 'Gandalf')
|
|
assert.equal(policy.deriveUsernameBase({ name: '💥', email: 'samwise@shire.net' }), 'samwise')
|
|
assert.equal(policy.deriveUsernameBase({ name: '', email: '' }), 'player')
|
|
// A reserved derived base is skipped in favor of the next candidate.
|
|
assert.equal(policy.deriveUsernameBase({ name: 'admin', email: 'realuser@x.com' }), 'realuser')
|
|
})
|
|
|
|
test('candidateUsername yields the base then increasing suffixes, clamped to length', () => {
|
|
assert.equal(policy.candidateUsername('bilbo', 0), 'bilbo')
|
|
assert.equal(policy.candidateUsername('bilbo', 1), 'bilbo2')
|
|
assert.equal(policy.candidateUsername('bilbo', 2), 'bilbo3')
|
|
// Long base: the numeric suffix must survive the MAX_LEN clamp.
|
|
const long = 'a'.repeat(policy.MAX_LEN)
|
|
const c = policy.candidateUsername(long, 10)
|
|
assert.ok(c.length <= policy.MAX_LEN)
|
|
assert.ok(c.endsWith('11'))
|
|
})
|