// Reserved-name screening (docs/website/TEAMS.md §2.8). // // Two failure modes with very different costs, and the tests are split along // that line: // // - a FALSE NEGATIVE puts an official-looking staff page on the operator's own // site, written by whoever typed a name into a guild stone; // - a FALSE POSITIVE hides a legitimate guild until a human glances at a queue. // // The second is cheap and recoverable, which is what lets the matcher be // conservative. It is not licence to be sloppy in the other direction: a check // that fires on "Badminton" is a check the operator switches off, and then the // first cost is paid in full. const { test, beforeEach, afterEach } = require('node:test') const assert = require('node:assert/strict') const settings = require('../src/model/settings/settings.model') const brand = require('../src/config/brand') const reserved = require('../src/utils/reservedNames') const saved = [] function patch(mod, name, fn) { saved.push([mod, name, mod[name]]) mod[name] = fn } beforeEach(() => { // A deployment with a two-word brand and no operator additions, which is the // shape that exercises the condensed-form rule. patch(settings, 'getInstanceName', async () => 'UO Mysticmoon') patch(settings, 'get', async () => null) }) afterEach(() => { while (saved.length) { const [mod, name, fn] = saved.pop() mod[name] = fn } }) const isReserved = async (name) => (await reserved.screen(name)).reserved const termFor = async (name) => (await reserved.screen(name)).term // ── The names this exists to catch ───────────────────────────────────────── test('bare role names are reserved', async () => { for (const name of ['Admin', 'admin', 'ADMIN', 'Moderator', 'Staff', 'Owner', 'GM', 'Administrator']) { assert.equal(await isReserved(name), true, `"${name}" must not become a public page`) } }) test('a role word inside a longer name is caught', async () => { for (const name of ['The Admin Team', 'Server Staff', 'GM Council', 'Guild of Moderators']) { assert.equal(await isReserved(name), true, `"${name}" is the impersonation this exists for`) } }) test('the deployment brand is reserved, in both presentations', async () => { assert.equal(await isReserved('UO Mysticmoon'), true) assert.equal(await isReserved('UOMysticmoon'), true, 'the condensed form is what an impersonator types') assert.equal(await isReserved('uo-mysticmoon'), true) assert.equal(await isReserved('UO_MYSTICMOON'), true) assert.equal(await isReserved('UOMysticmoon Staff'), true) }) test('the project name is reserved, in both of its legitimate presentations', async () => { // "Runic Gateway" is correct; "RunicGateway" is what the Gitea org and every // URL segment use, so it is the form someone would copy. assert.equal(await isReserved('Runic Gateway'), true) assert.equal(await isReserved('RunicGateway'), true) assert.equal(await isReserved('runic-gateway'), true) assert.equal(await isReserved('Runic_Gateway'), true) assert.equal(await isReserved('RUNIC GATEWAY'), true) }) test('operator additions are honoured', async () => { patch(settings, 'get', async (key) => (key === reserved.OPERATOR_TERMS_KEY ? 'Council, Arbiter' : null)) assert.equal(await isReserved('The Council'), true) assert.equal(await isReserved('Arbiter'), true) }) test('repeated characters are squeezed', async () => { assert.equal(await isReserved('Adminnn'), true) assert.equal(await isReserved('Staaaff'), true) }) test('punctuation between words does not evade the check', async () => { assert.equal(await isReserved('[Admin]'), true) assert.equal(await isReserved('~*~ Staff ~*~'), true) assert.equal(await isReserved('G.M.'), true) }) test('the matched term is reported, for the review queue', async () => { assert.equal(await termFor('The Admin Team'), 'admin') assert.equal(await termFor('UOMysticmoon'), 'UO Mysticmoon', 'shown in its stored form, not the input') }) // ── The names it must NOT catch ──────────────────────────────────────────── test('a word merely CONTAINING a reserved term is not reserved', async () => { // The scar tissue this rule comes from: checkModuleIdentifiers.js tokenises // precisely so `defaultImage` does not match "ultIma". for (const name of ['Badminton', 'Badminton Club', 'Modest Proposal', 'Gmork', 'Playerless']) { assert.equal(await isReserved(name), false, `"${name}" is a false positive that would discredit the check`) } }) test('ordinary guild names pass', async () => { for (const name of [ 'The Silver Hand', 'Knights of the Round', 'Dread Pirates', 'Moonlight Traders', 'The Guardians', 'Iron Wolves', ]) { assert.equal(await isReserved(name), false, `"${name}" is an ordinary guild`) } }) test('the condensed-form widening applies only to multi-word terms', async () => { // Running the letters together is safe for a two-word term because it is // specific; doing it for single-word terms is what would re-introduce // substring matching through the back door. assert.equal(await isReserved('Badminton'), false) assert.equal(await isReserved('Grandmaster'), false, 'contains "gm" only as a substring') assert.equal(await isReserved('Nomads'), false, 'contains "mod" only as a substring') }) test('an empty or unusable name is not reserved', async () => { for (const name of ['', ' ', null, undefined, '★☆★']) { assert.equal(await isReserved(name), false) } }) // ── Resolution is at check time, and fails safe ──────────────────────────── test('the brand is resolved at CHECK time, so a rename protects the new name', async () => { patch(settings, 'getInstanceName', async () => 'Dragonspire') assert.equal(await isReserved('Dragonspire'), true) patch(settings, 'getInstanceName', async () => 'Emberfall') assert.equal(await isReserved('Emberfall'), true, 'no redeploy should be needed to protect a new brand') }) test('a failed settings read falls back to the static terms rather than to none', async () => { // Screening fewer terms is bad; screening none is the entire hole. patch(settings, 'getInstanceName', async () => { throw new Error('db down') }) patch(settings, 'get', async () => { throw new Error('db down') }) assert.equal(await isReserved('Admin'), true, 'the role list must survive a database outage') assert.equal(await isReserved('Runic Gateway'), true) }) test('BRAND_NAME is covered even when no site_title is set', async () => { patch(settings, 'getInstanceName', async () => null) assert.equal(await isReserved(brand.name), true) }) test('the same term resolved twice is listed once', async () => { // The brand and an operator term are frequently the same word, and reporting // one match twice is noise in a queue a human reads. patch(settings, 'getInstanceName', async () => 'Dragonspire') patch(settings, 'get', async (key) => (key === reserved.OPERATOR_TERMS_KEY ? 'dragonspire' : null)) const terms = await reserved.reservedTerms() const normalised = terms.map((t) => reserved.normalise(t)) assert.equal(new Set(normalised).size, normalised.length) }) test('normalise folds case, diacritics and punctuation', () => { assert.equal(reserved.normalise('Ünderdärk!'), 'underdark') assert.equal(reserved.normalise(' The Silver Hand '), 'the silver hand') assert.equal(reserved.normalise('Adminnn'), 'admin', 'repeats are squeezed on both sides') }) test('plurals are caught, and near-misses are not', async () => { // "Moderators" is the more natural guild name of the two, so missing it would // miss the likelier case. for (const name of ['Moderators', 'The Admins', 'Guild of Moderators', 'Owners']) { assert.equal(await isReserved(name), true, `"${name}" impersonates as much as its singular`) } // Only a trailing s off the WHOLE term, so an ordinary word whose stem merely // contains one does not fire. for (const name of ['Nomads', 'Playerless', 'Gods']) { assert.equal(await isReserved(name), false, `"${name}" is not a plural of a reserved term`) } }) test('an acronym spelled with punctuation is caught', async () => { // "G.M." normalises to two single-letter words, neither of which is the term. assert.equal(await isReserved('G.M.'), true) assert.equal(await isReserved('G M Council'), true) // …but joining single letters must not condense whole names, which would let a // single-word term match inside an ordinary word again. assert.equal(await isReserved('Badminton'), false) })