Files
Module-Rust/server/test/playerPermissions.test.js
wtclaude 383e89442e feat(rust): what the site has given a player, as the player reads it
Phase 8's website half. Phase 7 made the site the author of in-game
privilege and gave an operator every view of it; this is the other side,
and it is the first time a player can see what they hold without asking
one.

`GET /player/rust/permissions` is self-scoped in SQL and read-only by
construction — a grant a player could change would not be a grant. Three
things make it a different shape from the admin read rather than a
filtered one:

  * the scope arithmetic is answered on the server. A client handed `*`
    would have to know what the fleet is to say anything, and then
    `inScope` exists twice. Each entry carries the servers it reaches,
    already resolved and already marked.
  * `live` is the pushed ledger, never the authored row. A grant is not a
    privilege in a game until a sync confirmed it, and phase 7 is careful
    never to record a push that silently did nothing — so "waiting" is
    honest, and the alternative is the site claiming to have given
    something it has not.
  * nothing says WHY it is waiting. An offline server, a permission no
    loaded plugin registered and a store that has never seen the account
    all look the same from here; telling them apart is an operator's
    diagnosis and an inventory of what is installed.

An entitlement that reaches nobody still lists, and the page says so —
authored against the website account, it exists before a Steam id does,
and hiding it until one turns up is the defect the admin user page
shipped in phase 7 (PLAN.md §20.5).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PMH6bw1jXMgbyF3ZWGEzSM
2026-09-22 20:09:51 -05:00

172 lines
6.3 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// ── What one player holds, as that player reads it ────────────────────────
//
// Phase 8's half of R2. The admin surface answers *who holds what* against the
// authored tables; this answers *what do I hold*, and the two differ in three
// ways that are each a test below:
//
// • the scope is RESOLVED here. A client handed `*` would have to know what
// the fleet is to say anything, and then `inScope` exists twice.
// • `live` is the PUSHED ledger, never the authored row. A grant is not a
// privilege in a game until a sync confirmed it, and phase 7 is careful
// never to record a push that silently did nothing — so "waiting" is an
// honest answer and the alternative is the site claiming to have given
// something it has not.
// • an entitlement reaching NOBODY still lists. Authored against the website
// account, it exists before a Steam id does, and hiding it until one turns
// up is the defect the admin user page shipped in phase 7 (PLAN.md §20.5).
const test = require('node:test')
const assert = require('node:assert')
const { fakeCtx } = require('./_fakes')
/** The model, wired to a db module answering from one fixture. */
function modelWith(fixture) {
require('../core')._reset()
require('../core').init(fakeCtx({ db: { query: () => Promise.resolve([]), pool: {} } }))
const db = require('../model/permissions/permissions.db')
const model = require('../model/permissions/permissions.model')
const originals = {}
for (const [name, value] of Object.entries(fixture)) {
originals[name] = db[name]
db[name] = () => Promise.resolve(value)
}
return { model, restore: () => Object.assign(db, originals) }
}
const SERVERS = [
{ id: 'main', name: 'Main' },
{ id: 'creative', name: 'Creative' },
]
/** One person: in a fleet group, holding one server-scoped grant. */
function fixture({ pushed = [] } = {}) {
return {
listGroupsForUser: [{ name: 'vip', title: 'VIP', rank: 10, scope: '*', addedAt: '2026-09-01T00:00:00Z' }],
listGroupPermissions: [
{ groupName: 'vip', permission: 'Kits.VIP' },
{ groupName: 'builder', permission: 'buildtools.use' },
],
listGrants: [
{ id: 7, userId: 4, permission: 'zonemanager.admin', scope: 'creative', source: 'admin', note: null, grantedAt: '2026-09-02T00:00:00Z', steamId: '7656119', playerName: 'Wanderer' },
],
listPushedForSteamIds: pushed,
}
}
test('a fleet scope resolves to every server; a server scope to one', async () => {
const { model, restore } = modelWith(fixture())
try {
const held = await model.forPlayer(4, ['7656119'], SERVERS)
assert.deepEqual(held.groups[0].reach.map((s) => s.id), ['main', 'creative'])
assert.deepEqual(held.grants[0].reach.map((s) => s.id), ['creative'])
} finally {
restore()
}
})
test('live is the pushed ledger, per server — not the authored row', async () => {
const { model, restore } = modelWith(
fixture({ pushed: [{ serverId: 'main', kind: 'member', subject: '7656119', object: 'vip' }] }),
)
try {
const held = await model.forPlayer(4, ['7656119'], SERVERS)
const byId = Object.fromEntries(held.groups[0].reach.map((s) => [s.id, s.live]))
assert.equal(byId.main, true, 'the server that confirmed it has it')
assert.equal(byId.creative, false, 'the one that has not is waiting, not live')
// The grant was never pushed anywhere, and an authored row must not imply one.
assert.deepEqual(held.grants[0].reach.map((s) => s.live), [false])
} finally {
restore()
}
})
test('an entitlement is live for the person when it landed on ANY account they hold', async () => {
// Two accounts, one membership pushed against the second. The game sees one
// player with the rank; so does this.
const { model, restore } = modelWith(
fixture({ pushed: [{ serverId: 'main', kind: 'member', subject: '7656120', object: 'vip' }] }),
)
try {
const held = await model.forPlayer(4, ['7656119', '7656120'], SERVERS)
assert.equal(held.groups[0].reach.find((s) => s.id === 'main').live, true)
} finally {
restore()
}
})
test('a grant and a membership are different rows about the same person', async () => {
// `kind` is why the pushed lookup carries it: a membership of `vip` and a
// direct grant named `vip` would otherwise be one entry in the map, and the
// wrong one would light up.
const { model, restore } = modelWith({
listGroupsForUser: [{ name: 'vip', title: 'VIP', rank: 0, scope: '*', addedAt: null }],
listGroupPermissions: [],
listGrants: [{ id: 1, userId: 4, permission: 'vip', scope: '*', source: 'admin', note: null, grantedAt: null, steamId: '7656119' }],
listPushedForSteamIds: [{ serverId: 'main', kind: 'grant', subject: '7656119', object: 'vip' }],
})
try {
const held = await model.forPlayer(4, ['7656119'], SERVERS)
assert.equal(held.grants[0].reach.find((s) => s.id === 'main').live, true)
assert.equal(held.groups[0].reach.find((s) => s.id === 'main').live, false)
} finally {
restore()
}
})
test('a player with no linked account still sees what they were given', async () => {
const { model, restore } = modelWith(fixture())
try {
const held = await model.forPlayer(4, [], SERVERS)
assert.equal(held.groups.length, 1)
assert.equal(held.grants.length, 1)
assert.ok(
[...held.groups[0].reach, ...held.grants[0].reach].every((s) => s.live === false),
'authored, and reaching nobody — which is the state worth showing',
)
} finally {
restore()
}
})
test('only the callers own groups carry their permissions, lowered as the store lowers them', async () => {
const { model, restore } = modelWith(fixture())
try {
const held = await model.forPlayer(4, ['7656119'], SERVERS)
// `builder`'s permission is in the group-permission table and this caller is
// not in that group; `Kits.VIP` is theirs, and arrives the way a game stores it.
assert.deepEqual(held.groups[0].permissions, ['kits.vip'])
} finally {
restore()
}
})
test('a fleet with no servers configured reaches nothing and does not throw', async () => {
const { model, restore } = modelWith(fixture())
try {
const held = await model.forPlayer(4, ['7656119'], [])
assert.deepEqual(held.groups[0].reach, [])
assert.deepEqual(held.grants[0].reach, [])
} finally {
restore()
}
})