Files
Module-Rust/server/test/visibility.test.js
wtclaude cc185db26b feat(rust): the rewards — tally, kit reward, chat and the news leg (phase 13b, protocol 10)
Four event verbs and the announce leg, per PLAN.md §29:

- rust.participation.open / .collect: the plugin counts who takes part
  (seconds, kills or both, in a zone this run opened or the whole server)
  and collect files them as the run's participants, keyed by Steam id.
- rust.kit.entitle: the five recipient modes (D101), rows in the new
  rust_perm_run_grants (D84) unioned into the permission push, one extra
  use of the kit per reward as site-held credits on perm.sync (D103),
  and the rust.kit.entitled notice deferred from phase 10 (D64).
- rust.announce: one server or every server (D105).
- rust.chat announce leg, speaking only on servers whose new news switch
  is on (D104) - a card on Admin -> Rust visibility (D106).

Budgets rust.grants and rust.announcements; the kit source and four
fixed-choice sources (core has no enum param type). rust_perm_run_grants
carries core's idempotency key so a revert of a lost answer can find its
rows. Protocol 10.

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

310 lines
12 KiB
JavaScript

// ── Who may see who is online ─────────────────────────────────────────────
//
// The org lead's rule (2026-09-22): nothing tells who is online by default. The
// suite holds the four properties that make that rule true rather than merely
// intended:
//
// • an install nobody has configured answers STAFF;
// • the viewer's standing comes from the ROW, not the token — a demotion or a
// ban takes effect on the next request;
// • anything unrecognised or unanswerable narrows, never widens;
// • the public routes answer the count and withhold the names.
const test = require('node:test')
const assert = require('node:assert')
const { fakeCtx, spy } = require('./_fakes')
/**
* The model with a stubbed db and a chosen viewer.
*
* `claimed` is what the token says; `row` is what the users table says now.
*/
function setup({ fleet = null, overrides = {}, claimed = null, row = null, usersThrow = false } = {}) {
require('../core')._reset()
require('../core').init(
fakeCtx({
auth: { getUserFromRequest: () => claimed },
users: {
getById: async () => {
if (usersThrow) throw new Error('pool exhausted')
return row
},
},
}),
)
const db = require('../model/visibility/visibility.db')
const model = require('../model/visibility/visibility.model')
const written = { settings: [], servers: [] }
const originals = { ...db }
db.getSetting = async () => fleet
db.setSetting = async (key, value, userId) => written.settings.push({ key, value, userId })
db.getServerPresence = async (id) => (id in overrides ? overrides[id] : undefined)
db.listServerPresence = async () =>
Object.entries(overrides).map(([id, presence]) => ({ id, name: id.toUpperCase(), enabled: 1, presence }))
db.setServerPresence = async (id, value) => written.servers.push({ id, value })
return { model, written, restore: () => Object.assign(db, originals) }
}
test('an install nobody has configured shows the roll call to staff and nobody else', async () => {
const { model, restore } = setup({ overrides: { main: null } })
try {
assert.equal(await model.fleetPresence(), 'staff')
assert.equal(await model.presenceFor('main'), 'staff')
assert.equal((await model.canSeePresence({}, 'main')).visible, false, 'anonymous')
} finally {
restore()
}
})
test('the standing comes from the row, not the token', async () => {
// The token says moderator; the row says they were demoted this morning.
const demoted = setup({ claimed: { id: 4, role: 'moderator' }, row: { id: 4, role: 'player', status: 'active' } })
try {
assert.equal(await demoted.model.viewerLevel({}), 'signed_in')
} finally {
demoted.restore()
}
// The token says admin; the account has been banned since.
const banned = setup({ claimed: { id: 4, role: 'admin' }, row: { id: 4, role: 'admin', status: 'banned' } })
try {
assert.equal(await banned.model.viewerLevel({}), 'public')
} finally {
banned.restore()
}
const moderator = setup({ claimed: { id: 5 }, row: { id: 5, role: 'moderator', status: 'active' } })
try {
assert.equal(await moderator.model.viewerLevel({}), 'staff')
} finally {
moderator.restore()
}
})
test('a viewer who cannot be resolved is anonymous', async () => {
const gone = setup({ claimed: { id: 9 }, row: null })
try {
assert.equal(await gone.model.viewerLevel({}), 'public')
} finally {
gone.restore()
}
const failing = setup({ claimed: { id: 9 }, usersThrow: true })
try {
assert.equal(await failing.model.viewerLevel({}), 'public')
} finally {
failing.restore()
}
})
test('a stored value this build does not recognise narrows to staff', async () => {
const { model, restore } = setup({ fleet: 'everyone', overrides: { main: 'PUBLIC', pvp: null } })
try {
assert.equal(await model.fleetPresence(), 'staff')
assert.equal(await model.presenceFor('main'), 'staff', 'a mis-cased word is not "public"')
assert.equal(await model.presenceFor('pvp'), 'staff', 'inherits the (narrowed) fleet default')
} finally {
restore()
}
})
test('a server override wins over the fleet, and null inherits it', async () => {
const { model, restore } = setup({
fleet: 'signed_in',
overrides: { main: 'public', pvp: 'staff', creative: null },
claimed: { id: 4 },
row: { id: 4, role: 'player', status: 'active' },
})
try {
assert.equal(await model.presenceFor('main'), 'public')
assert.equal(await model.presenceFor('pvp'), 'staff')
assert.equal(await model.presenceFor('creative'), 'signed_in')
// A signed-in player sees main and creative, not pvp.
assert.equal((await model.canSeePresence({}, 'main')).visible, true)
assert.equal((await model.canSeePresence({}, 'creative')).visible, true)
assert.equal((await model.canSeePresence({}, 'pvp')).visible, false)
const described = await model.describe()
const byId = Object.fromEntries(described.presence.servers.map((s) => [s.id, s]))
assert.equal(byId.creative.override, null)
assert.equal(byId.creative.effective, 'signed_in')
assert.equal(byId.pvp.effective, 'staff')
} finally {
restore()
}
})
test('an update naming an unknown audience or server writes nothing at all', async () => {
const { model, written, restore } = setup({ overrides: { main: null } })
try {
const badAudience = await model.update({ fleet: 'public', servers: { main: 'everyone' } })
assert.equal(badAudience.ok, false)
assert.equal(badAudience.status, 400)
const badServer = await model.update({ fleet: 'public', servers: { main: 'public', nope: 'public' } })
assert.equal(badServer.ok, false)
assert.equal(badServer.status, 404)
assert.match(badServer.message, /nope/)
assert.deepEqual(written, { settings: [], servers: [] }, 'validated whole before anything was written')
const ok = await model.update({ fleet: 'signed_in', servers: { main: null } }, { id: 1 })
assert.equal(ok.ok, true)
assert.deepEqual(written.settings, [{ key: 'presence.audience', value: 'signed_in', userId: 1 }])
assert.deepEqual(written.servers, [{ id: 'main', value: null }])
assert.deepEqual(ok.changed, { fleet: 'signed_in', servers: { main: 'inherit' } })
} finally {
restore()
}
})
test('the clan roster audience defaults to members, and a bad one writes nothing (D48)', async () => {
const { model, written, restore } = setup({ overrides: { main: null } })
try {
// Nothing stored: the clan's own members and staff. The presence value the
// stub answers ('staff' is not a clan rung) must not leak across keys.
assert.equal(await model.clanRosterAudience(), 'members')
assert.equal((await model.describe()).clans.roster, 'members')
const bad = await model.update({ clanRoster: 'staff' })
assert.equal(bad.ok, false)
assert.equal(bad.status, 400)
assert.deepEqual(written.settings, [])
const ok = await model.update({ clanRoster: 'signed_in' }, { id: 7 })
assert.equal(ok.ok, true)
assert.deepEqual(written.settings, [{ key: 'clans.roster.audience', value: 'signed_in', userId: 7 }])
assert.equal(ok.changed.clanRoster, 'signed_in')
} finally {
restore()
}
})
test('news in game chat is off by default, on or off per server, and validated whole (D104, D106)', async () => {
const { model, written, restore } = setup({ overrides: { main: null } })
const db = require('../model/visibility/visibility.db')
const news = []
db.setServerNews = async (id, on) => news.push({ id, on })
try {
// A row that never had the column set reads as off.
assert.deepEqual((await model.describe()).news.servers, [{ id: 'main', name: 'MAIN', enabled: true, on: false }])
const notBoolean = await model.update({ news: { main: 'yes' } })
assert.equal(notBoolean.status, 400)
const unknown = await model.update({ fleet: 'public', news: { main: true, nope: true } })
assert.equal(unknown.status, 404)
assert.deepEqual(news, [])
assert.deepEqual(written.settings, [], 'the fleet change beside it was not written either')
const ok = await model.update({ news: { main: true } }, { id: 3 })
assert.equal(ok.ok, true)
assert.deepEqual(news, [{ id: 'main', on: true }])
assert.deepEqual(ok.changed.news, { main: true })
} finally {
restore()
}
})
// ── The public routes ─────────────────────────────────────────────────────
/** A response double recording what a handler answered. */
function fakeRes() {
const res = {
statusCode: 200,
headers: {},
varied: [],
body: undefined,
status(code) { this.statusCode = code; return this },
json(body) { this.body = body; return this },
set(name, value) { this.headers[name.toLowerCase()] = value; return this },
vary(name) { this.varied.push(name); return this },
}
return res
}
function withPresence(visible) {
const visibility = require('../model/visibility/visibility.model')
const events = require('../model/events/events.model')
const servers = require('../model/servers/servers.model')
const originals = {
canSeePresence: visibility.canSeePresence,
online: events.online,
getPublic: servers.getPublic,
}
visibility.canSeePresence = async () => ({ visible, level: visible ? 'staff' : 'public', required: 'staff' })
events.online = spy(Promise.resolve([{ steamId: '7656', name: 'Wanderer', sleeping: false, connectedAt: null }]))
servers.getPublic = async () => ({ id: 'main', players: 12 })
return {
events,
restore: () => {
visibility.canSeePresence = originals.canSeePresence
events.online = originals.online
servers.getPublic = originals.getPublic
},
}
}
test('below the audience, the Online list answers the count and never reads the names', async () => {
require('../core')._reset()
require('../core').init(fakeCtx())
const { events, restore } = withPresence(false)
try {
const controller = require('../router/public/rust.controller')
const res = fakeRes()
await controller.listOnline({ params: { id: 'main' } }, res)
assert.deepEqual(res.body, { players: [], hidden: true, count: 12, audience: 'staff' })
assert.equal(events.online.calls.length, 0, 'the names are not even read')
assert.equal(res.headers['cache-control'], 'private, no-store', 'a per-viewer answer must not be shared by a cache')
} finally {
restore()
}
})
test('inside the audience, the Online list names the players', async () => {
require('../core')._reset()
require('../core').init(fakeCtx())
const { restore } = withPresence(true)
try {
const controller = require('../router/public/rust.controller')
const res = fakeRes()
await controller.listOnline({ params: { id: 'main' } }, res)
assert.equal(res.body.hidden, false)
assert.equal(res.body.players[0].name, 'Wanderer')
} finally {
restore()
}
})
test('below the audience, the feed says it withheld the players rather than implying a quiet server', async () => {
require('../core')._reset()
require('../core').init(fakeCtx())
const { restore } = withPresence(false)
const events = require('../model/events/events.model')
const original = events.recent
let asked = null
events.recent = async (args) => {
asked = args
return []
}
try {
const controller = require('../router/public/rust.controller')
const res = fakeRes()
await controller.listEvents({ params: { id: 'main' }, query: {} }, res)
assert.equal(asked.presence, false)
assert.equal(asked.admin, undefined, 'the public route never passes admin')
assert.equal(res.body.presenceHidden, true)
assert.equal(res.body.presenceAudience, 'staff')
} finally {
events.recent = original
restore()
}
})