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
310 lines
12 KiB
JavaScript
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()
|
|
}
|
|
})
|