Files
Module-Rust/server/test/events.test.js
wtclaude be44839896
All checks were successful
PR Checks / client-build (pull_request) Successful in 27s
PR Checks / frozen-manifest (pull_request) Successful in 51s
PR Checks / server-tests (pull_request) Successful in 8m6s
fix(rust): nothing names who is online by default
The org lead's rule, settled 2026-09-22: who is online is always the
narrowest audience - staff - unless an operator deliberately widens it,
and a count is fine where a list of names is not.

The public site broke that in three places since phase 4. The Online
tab named every player, the feed carried joins, respawns, deaths, chat
and tallies, and the leaderboard's lastSeen - refreshed every minute by
a gather tally - said who was on as plainly as either. All three now
sit behind one setting:

* PRESENCE_KINDS, a subset of the public allowlist, gated per request.
  Below the audience the feed keeps the server's own story (wipe, start,
  shutdown) and says presenceHidden rather than looking quiet.
* the Online route answers { players: [], hidden, count, audience } -
  same shape, so an older client renders empty rather than breaking.
* rungs staff / signed_in / public, fleet-wide default in a new
  rust_settings table with an optional per-server override on
  rust_servers; an unknown stored word narrows to staff.
* the viewer's standing is RE-READ from the users row (ctx.users.getById),
  not taken from the token, so a demotion or a ban applies on the next
  request. Walked: a moderator demoted mid-session lost the roll call on
  the same cookie.
* per-viewer answers are Cache-Control: private, no-store.
* GET/PUT /admin/rust/visibility (requireRole admin) and an admin page,
  Rust visibility; every save is one activity-log row.

The browser walk also found every empty state in this module rendering
as a blank box. Core's EmptyState renders children only; this module
passed title/message (the shape the Integration Kit template teaches)
and React dropped both without a word. Fixed module-side with a small
Empty wrapper - nothing core or module-uo renders changes - and a client
test that refuses a titled EmptyState or a PageHeader subtitle.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01E14m6SuuY6i1vASFeGDBeY
2026-09-23 00:30:08 -05:00

173 lines
5.8 KiB
JavaScript

// ── The read path's logic ─────────────────────────────────────────────────
//
// The model decides what a caller gets. Two properties are worth more than the
// rest, and both are about a caller who did something slightly wrong:
//
// • a route that forgets to say who is asking gets the PUBLIC view;
// • a caller asking for a million rows gets two hundred.
const test = require('node:test')
const assert = require('node:assert')
const { fakeCtx } = require('./_fakes')
function withCore() {
require('../core')._reset()
require('../core').init(fakeCtx())
}
test('the limit is bounded, whatever was asked for', () => {
withCore()
const model = require('../model/events/events.model')
assert.equal(model.boundedLimit(10), 10)
assert.equal(model.boundedLimit(undefined), 50)
assert.equal(model.boundedLimit('nonsense'), 50)
assert.equal(model.boundedLimit(-5), 50)
assert.equal(model.boundedLimit(0), 50)
assert.equal(model.boundedLimit(1e9), model.MAX_LIMIT)
assert.equal(model.boundedLimit(12.9), 12)
})
test('kinds parse from one name or a list, and nothing means "not specified"', () => {
withCore()
const model = require('../model/events/events.model')
assert.deepEqual(model.parseKinds('player.death'), ['player.death'])
assert.deepEqual(model.parseKinds('player.death, player.chat'), ['player.death', 'player.chat'])
// Null rather than an empty list: "I did not ask" and "I asked for nothing"
// are different, and only the first means "whatever I am allowed".
assert.equal(model.parseKinds(''), null)
assert.equal(model.parseKinds(undefined), null)
assert.equal(model.parseKinds(' , , '), null)
})
test('a reader who does not say who they are gets the public view', async () => {
withCore()
const db = require('../model/events/events.db')
const model = require('../model/events/events.model')
const original = db.recentEvents
let asked = null
db.recentEvents = async (args) => {
asked = args
return []
}
try {
await model.recent({ serverId: 'main' })
assert.ok(!asked.kinds.includes('player.banned'), 'no IP-carrying kind by default')
// Nor anything naming a player who was on — the org lead's rule, and a caller
// that forgets to say what the viewer may see gets the narrowest answer.
assert.ok(!asked.kinds.includes('player.death'), 'no presence kind by default')
assert.ok(asked.kinds.includes('server.wipe'))
await model.recent({ serverId: 'main', presence: true })
assert.ok(asked.kinds.includes('player.death'), 'a viewer inside the presence audience gets the killfeed')
assert.ok(!asked.kinds.includes('player.banned'), 'presence never widens to staff kinds')
await model.recent({ serverId: 'main', admin: true })
assert.ok(asked.kinds.includes('player.banned'), 'an admin who says so gets them')
} finally {
db.recentEvents = original
}
})
test('asking only for kinds you may not see answers with nothing, and queries nothing', async () => {
withCore()
const db = require('../model/events/events.db')
const model = require('../model/events/events.model')
const original = db.recentEvents
let called = false
db.recentEvents = async () => {
called = true
return []
}
try {
const rows = await model.recent({ serverId: 'main', kind: 'player.banned,player.approved' })
assert.deepEqual(rows, [])
assert.equal(called, false, 'a query with no permitted kinds must not reach the database')
} finally {
db.recentEvents = original
}
})
test('a row whose stored frame will not parse still answers with its envelope', async () => {
withCore()
const db = require('../model/events/events.db')
const model = require('../model/events/events.model')
const original = db.recentEvents
db.recentEvents = async () => [
{ id: 7, kind: 'player.death', t: 12, wipeId: 'w-1', steamId: 'p1', raw: '{not json' },
]
try {
const [row] = await model.recent({ serverId: 'main' })
// One unreadable row must not fail a whole page. What is known is still
// reported; the body is empty rather than absent.
assert.equal(row.id, 7)
assert.equal(row.kind, 'player.death')
assert.deepEqual(row.frame, {})
} finally {
db.recentEvents = original
}
})
test('the leaderboard answers numbers, never nulls', async () => {
withCore()
const db = require('../model/events/events.db')
const model = require('../model/events/events.model')
const original = db.leaderboard
// SUM() over no rows is NULL in SQL, and a JOIN with no player row gives a
// null name. A page that has to defend against both is a page with the
// defence in three places.
db.leaderboard = async () => [
{ steamId: 'p1', name: null, kills: null, deaths: '3', npcKills: null, playtimeSec: null },
]
try {
const [row] = await model.leaderboard({ serverId: 'main' })
assert.equal(row.kills, 0)
assert.equal(row.deaths, 3)
assert.equal(row.npcKills, 0)
assert.equal(row.playtimeSec, 0)
assert.equal(row.name, null)
} finally {
db.leaderboard = original
}
})
test('the leaderboard withholds lastSeen unless the viewer may see who is online', async () => {
withCore()
const db = require('../model/events/events.db')
const model = require('../model/events/events.model')
const original = db.leaderboard
db.leaderboard = async () => [{ steamId: '7656', name: 'A', kills: 3, lastSeen: '2026-09-22T10:00:00Z' }]
try {
const hidden = await model.leaderboard({ serverId: 'main' })
assert.equal('lastSeen' in hidden[0], false, 'absent, not null — null would read as "never seen"')
assert.equal(hidden[0].kills, 3)
const shown = await model.leaderboard({ serverId: 'main', presence: true })
assert.equal(shown[0].lastSeen, '2026-09-22T10:00:00Z')
} finally {
db.leaderboard = original
}
})