The live walk rendered a generic in-app notice as "A server came online.
A server's game started..." Core's structural projection falls back to
the trigger's label and description when the payload has no title, and
on a multi-server site that never says which server. Core's rule is that
the payload wins, so every trigger now declares `title` and `intro`, and
the emitter writes the sentence ("Oxide rig is online"). An operator's
own template can still ignore it and use the parts.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01E14m6SuuY6i1vASFeGDBeY
493 lines
21 KiB
JavaScript
493 lines
21 KiB
JavaScript
// ── Notifications and engagement (phase 10, PLAN.md §25) ───────────────────
|
|
//
|
|
// The properties this suite holds, each with a failure behind it:
|
|
//
|
|
// • every declaration is one core will accept — a ceiling, a subjectKey that
|
|
// names a declared variable, an example on every variable, a closed type —
|
|
// because core refuses the WHOLE module at boot over one bad declaration;
|
|
// • no trigger declares an address, or the raider (D66);
|
|
// • the raid alert reaches exactly the authorised, linked people, one emit
|
|
// each with `ownerUserId`, and nobody when there is no cupboard (D59, D67);
|
|
// • a replayed event is told only while it is still news (D63);
|
|
// • a clan notice goes to the clan and never to whoever caused it;
|
|
// • a transition is announced once, and a first sighting never;
|
|
// • a tie at the top of the leaderboard is not a new leader;
|
|
// • every seed body interpolates only variables its trigger declares.
|
|
|
|
const test = require('node:test')
|
|
const assert = require('node:assert')
|
|
|
|
const { fakeCtx, spy } = require('./_fakes')
|
|
|
|
// Core's own check on a `url` variable (utils/engagementEmit.js RELATIVE_URL),
|
|
// copied so a path this module builds is held to the rule it will meet.
|
|
const RELATIVE_URL = /^\/(?!\/)[A-Za-z0-9\-._~/?#[\]@!$&'()*+,;=%]*$/
|
|
|
|
const CEILINGS = ['everyone', 'authenticated', 'subscribers', 'members', 'staff', 'admin', 'owner']
|
|
const TYPES = ['string', 'int', 'float', 'boolean', 'datetime', 'url']
|
|
|
|
const SERVER = { id: 'main', name: 'Main' }
|
|
const NOW = Date.now()
|
|
|
|
/** A fresh ctx, a recording emit, and a link table the tests control. */
|
|
function setup({ links = {}, members = {}, state = null, board = [] } = {}) {
|
|
require('../core')._reset()
|
|
const ctx = fakeCtx()
|
|
require('../core').init(ctx)
|
|
|
|
const emit = require('../engagement/emit')
|
|
emit.reset()
|
|
|
|
const linksDb = require('../model/links/links.db')
|
|
const clansDb = require('../model/clans/clans.db')
|
|
const clans = require('../model/clans/clans.model')
|
|
const serversDb = require('../model/servers/servers.db')
|
|
const eventsDb = require('../model/events/events.db')
|
|
|
|
const originals = [
|
|
[linksDb, { ...linksDb }], [clansDb, { ...clansDb }], [clans, { ...clans }],
|
|
[serversDb, { ...serversDb }], [eventsDb, { ...eventsDb }],
|
|
]
|
|
|
|
linksDb.userIdsForSteamIds = async (ids) =>
|
|
ids.filter((id) => links[id]).map((id) => ({ steamId: id, userId: links[id] }))
|
|
clans.resolveExternalId = async (serverId, frame) => (frame.clanId ? `${serverId}:${frame.clanId}:1` : null)
|
|
clansDb.listMembers = async (externalId) => (members[externalId] || []).map((steamId) => ({ steamId }))
|
|
serversDb.getState = async () => state
|
|
eventsDb.leaderboard = async () => board.shift() || []
|
|
|
|
const restore = () => {
|
|
for (const [mod, copy] of originals) Object.assign(mod, copy)
|
|
}
|
|
|
|
return { emit, calls: ctx.events.emit.calls, restore, eventsDb }
|
|
}
|
|
|
|
const raidFrame = (extra = {}) => ({
|
|
kind: 'entity.destroyed',
|
|
t: NOW,
|
|
ownerId: '100',
|
|
prefab: 'door.hinged.metal',
|
|
structure: 'door',
|
|
attackerId: '900',
|
|
attackerName: 'Raider',
|
|
grid: 'H7',
|
|
buildingId: '8113',
|
|
authorized: [
|
|
{ steamId: '101', online: false },
|
|
{ steamId: '102', online: true },
|
|
{ steamId: '103', online: false },
|
|
],
|
|
...extra,
|
|
})
|
|
|
|
// ── The declarations ───────────────────────────────────────────────────────
|
|
|
|
test('every trigger is a declaration core will accept', () => {
|
|
const { TRIGGERS } = require('../engagement/triggers')
|
|
const ids = new Set()
|
|
|
|
for (const t of TRIGGERS) {
|
|
assert.ok(t.id.startsWith('rust.'), `${t.id} is namespaced`)
|
|
assert.ok(!ids.has(t.id), `${t.id} is declared once`)
|
|
ids.add(t.id)
|
|
assert.ok(CEILINGS.includes(t.ceiling), `${t.id} has a real ceiling (there is no "self")`)
|
|
assert.ok(CEILINGS.includes(t.audience), `${t.id} has a real default audience`)
|
|
assert.strictEqual(t.kind, 'event')
|
|
|
|
const names = new Set(t.variables.map((v) => v.name))
|
|
assert.ok(names.has(t.subjectKey), `${t.id}'s subjectKey names a declared variable`)
|
|
|
|
for (const v of t.variables) {
|
|
assert.ok(TYPES.includes(v.type), `${t.id}.${v.name} has a closed type`)
|
|
assert.ok(v.example !== undefined, `${t.id}.${v.name} has an example`)
|
|
if (v.type === 'url') assert.match(v.example, RELATIVE_URL, `${t.id}.${v.name}'s example is site-relative`)
|
|
}
|
|
}
|
|
})
|
|
|
|
test('the default audience is never wider than the ceiling', () => {
|
|
const { TRIGGERS } = require('../engagement/triggers')
|
|
// The pairs this catalogue uses, each one core's `permits` accepts. A new
|
|
// pairing is a new line here — decided, not assumed.
|
|
const ALLOWED = new Set(['everyone>subscribers', 'owner>owner', 'members>members', 'staff>staff'])
|
|
for (const t of TRIGGERS) {
|
|
assert.ok(ALLOWED.has(`${t.ceiling}>${t.audience}`), `${t.id}: ${t.audience} under ${t.ceiling}`)
|
|
}
|
|
})
|
|
|
|
test('no trigger declares an address, or who raided whom (D66)', () => {
|
|
const { TRIGGERS } = require('../engagement/triggers')
|
|
for (const t of TRIGGERS) {
|
|
for (const v of t.variables) {
|
|
// By camelCase WORD: `wipeId` contains the letters "ip" and is not one.
|
|
const words = v.name.split(/(?=[A-Z])/).map((w) => w.toLowerCase())
|
|
for (const banned of ['ip', 'address', 'attacker', 'raider']) {
|
|
assert.ok(!words.includes(banned), `${t.id}.${v.name}`)
|
|
}
|
|
}
|
|
}
|
|
})
|
|
|
|
test('the ceilings are the ones §25.2 decided', () => {
|
|
const { TRIGGERS } = require('../engagement/triggers')
|
|
const by = Object.fromEntries(TRIGGERS.map((t) => [t.id, t.ceiling]))
|
|
assert.strictEqual(by['rust.base.destroyed'], 'owner')
|
|
assert.strictEqual(by['rust.player.linked'], 'owner')
|
|
for (const id of ['rust.player.reported', 'rust.player.banned', 'rust.player.unbanned', 'rust.login.denied']) {
|
|
assert.strictEqual(by[id], 'staff', id)
|
|
}
|
|
for (const id of ['rust.clan.member.left', 'rust.clan.member.kicked', 'rust.clan.disbanded']) {
|
|
assert.strictEqual(by[id], 'members', id)
|
|
}
|
|
// D64: core's team.member.joined already covers it, and kits wait for phase 13.
|
|
assert.strictEqual(by['rust.clan.member.added'], undefined)
|
|
assert.strictEqual(by['rust.kit.entitled'], undefined)
|
|
})
|
|
|
|
test('a clan path survives core\'s url check, colons and all', () => {
|
|
const { clanPath, serverPath, leaderboardPath } = require('../engagement/triggers')
|
|
assert.match(clanPath('main:12:1790142840000'), RELATIVE_URL)
|
|
assert.match(serverPath('eu 2'), RELATIVE_URL)
|
|
assert.match(leaderboardPath('main'), RELATIVE_URL)
|
|
})
|
|
|
|
test('every trigger names what happened and where, even with its optionals missing', () => {
|
|
// The walk found a multi-server site's generic notice reading "A server came
|
|
// online" — core's projection falls back to the LABEL when the payload carries
|
|
// no `title`. Every trigger therefore declares its own, and the emitter writes
|
|
// it; a headline that printed "undefined" would be worse than the label.
|
|
const { TRIGGERS } = require('../engagement/triggers')
|
|
const { headline } = require('../engagement/emit')
|
|
for (const t of TRIGGERS) {
|
|
const minimal = Object.fromEntries(t.variables.filter((v) => v.required).map((v) => [v.name, v.example]))
|
|
const h = headline(t.id, minimal)
|
|
assert.ok(h.title && h.intro, `${t.id} has a headline`)
|
|
assert.ok(!/undefined|null/.test(h.title + h.intro), `${t.id}: ${h.title} / ${h.intro}`)
|
|
}
|
|
const online = headline('rust.server.online', { server: 'EU 2' })
|
|
assert.match(online.title, /EU 2/, 'the notice says WHICH server')
|
|
})
|
|
|
|
// ── The seeds ──────────────────────────────────────────────────────────────
|
|
|
|
test('every seeded rule is ours, off, and names bodies that exist', () => {
|
|
const { TRIGGERS } = require('../engagement/triggers')
|
|
const { TEMPLATES, RULE_GROUPS } = require('../engagement/seeds')
|
|
const triggerIds = new Set(TRIGGERS.map((t) => t.id))
|
|
const own = new Set(TEMPLATES.map((t) => t.key))
|
|
const coreKeys = new Set(['notify.event', 'inapp.event', 'notify.digest'])
|
|
const groupKeys = new Set()
|
|
|
|
for (const group of RULE_GROUPS) {
|
|
assert.ok(!groupKeys.has(group.key), `group ${group.key} is unique`)
|
|
groupKeys.add(group.key)
|
|
for (const r of group.rules) {
|
|
assert.ok(triggerIds.has(r.trigger_id), `${r.trigger_id} is one of ours`)
|
|
assert.strictEqual(r.enabled, undefined, 'enabled is never a seed parameter')
|
|
assert.ok(Number.isInteger(r.max_sends_per_hour) && r.max_sends_per_hour >= 1)
|
|
for (const [slot, key] of Object.entries(r.template_keys)) {
|
|
assert.ok(own.has(key) || coreKeys.has(key), `${r.trigger_id}: ${key}`)
|
|
if (slot !== 'digest') assert.ok(r.channels.includes(slot), `${r.trigger_id}: ${slot} is a channel`)
|
|
}
|
|
for (const channel of r.channels) {
|
|
if (channel !== 'push') assert.ok(r.template_keys[channel], `${r.trigger_id}: a body for ${channel}`)
|
|
}
|
|
}
|
|
}
|
|
})
|
|
|
|
test('push appears only on the rules whose trigger is also a stream (D65)', () => {
|
|
const { STREAMS } = require('../engagement/streams')
|
|
const { RULE_GROUPS } = require('../engagement/seeds')
|
|
const pushable = new Set(STREAMS.map((s) => s.id))
|
|
for (const group of RULE_GROUPS) {
|
|
for (const r of group.rules) {
|
|
if (r.channels.includes('push')) assert.ok(pushable.has(r.trigger_id), r.trigger_id)
|
|
}
|
|
}
|
|
})
|
|
|
|
test('every body interpolates only what its trigger declares', () => {
|
|
const { TRIGGERS } = require('../engagement/triggers')
|
|
const { TEMPLATES } = require('../engagement/seeds')
|
|
const declared = new Map(TRIGGERS.map((t) => [t.id, new Set(t.variables.map((v) => v.name))]))
|
|
// Core's per-delivery and ambient variables — supplied by the renderer.
|
|
const ambient = new Set(['unsubscribeUrl', 'siteName', 'siteUrl', 'logoUrl', 'year'])
|
|
|
|
for (const t of TEMPLATES) {
|
|
const vars = declared.get(t.triggerId)
|
|
assert.ok(vars, `${t.key}'s trigger exists`)
|
|
const text = JSON.stringify([t.subject, t.blocks])
|
|
for (const [, name] of text.matchAll(/\{\{\s*([A-Za-z0-9_]+)\s*\}\}/g)) {
|
|
assert.ok(vars.has(name) || ambient.has(name), `${t.key} uses {{${name}}}`)
|
|
}
|
|
assert.ok(t.key.startsWith('rust.'))
|
|
if (t.channel === 'email') assert.ok(t.subject)
|
|
else assert.strictEqual(t.subject, null)
|
|
}
|
|
})
|
|
|
|
test('the seeded raid rule is the OFFLINE raid alert, as a condition (D61)', () => {
|
|
const { RULE_GROUPS } = require('../engagement/seeds')
|
|
const raid = RULE_GROUPS.find((g) => g.key === 'raid-v1').rules[0]
|
|
assert.deepStrictEqual(raid.conditions, { variable: 'ownerOnline', cmp: 'eq', value: false })
|
|
assert.strictEqual(raid.audience, 'owner')
|
|
})
|
|
|
|
// ── The raid alert ─────────────────────────────────────────────────────────
|
|
|
|
test('the raid alert reaches each authorised, linked person, and nobody else', async () => {
|
|
const { emit, calls, restore } = setup({ links: { 101: 11, 102: 12 } })
|
|
try {
|
|
const sent = await emit.onEvent(SERVER, { id: 1, kind: 'entity.destroyed', frame: raidFrame() })
|
|
assert.strictEqual(sent, 2)
|
|
assert.deepStrictEqual(calls.map((c) => c[1].ownerUserId).sort(), [11, 12])
|
|
|
|
for (const [trigger, env] of calls) {
|
|
assert.strictEqual(trigger, 'rust.base.destroyed')
|
|
assert.strictEqual(env.recipientUserIds, undefined, 'owner-shaped, never a recipient list')
|
|
assert.strictEqual(env.data.building, '8113')
|
|
assert.strictEqual(env.data.structure, 'door')
|
|
assert.strictEqual(env.data.atGrid, ' in H7')
|
|
assert.ok(!JSON.stringify(env.data).includes('Raider'), 'the raider is never named')
|
|
assert.ok(!JSON.stringify(env.data).includes('900'))
|
|
}
|
|
const online = Object.fromEntries(calls.map((c) => [c[1].ownerUserId, c[1].data.ownerOnline]))
|
|
assert.deepStrictEqual(online, { 11: false, 12: true })
|
|
} finally {
|
|
restore()
|
|
}
|
|
})
|
|
|
|
test('two Steam accounts held by one person are one alert, online if either is', async () => {
|
|
const { emit, calls, restore } = setup({ links: { 101: 11, 102: 11 } })
|
|
try {
|
|
await emit.onEvent(SERVER, { kind: 'entity.destroyed', frame: raidFrame() })
|
|
assert.strictEqual(calls.length, 1)
|
|
assert.strictEqual(calls[0][1].data.ownerOnline, true)
|
|
} finally {
|
|
restore()
|
|
}
|
|
})
|
|
|
|
test('an authorised attacker is demolishing their own base: no alert', async () => {
|
|
const { emit, calls, restore } = setup({ links: { 101: 11, 102: 12 } })
|
|
try {
|
|
await emit.onEvent(SERVER, { kind: 'entity.destroyed', frame: raidFrame({ attackerId: '102' }) })
|
|
assert.strictEqual(calls.length, 0)
|
|
} finally {
|
|
restore()
|
|
}
|
|
})
|
|
|
|
test('no cupboard, nobody to tell — and a protocol-6 frame is the same (D67)', async () => {
|
|
const { emit, calls, restore } = setup({ links: { 100: 10, 101: 11 } })
|
|
try {
|
|
await emit.onEvent(SERVER, { kind: 'entity.destroyed', frame: raidFrame({ buildingId: undefined, authorized: undefined }) })
|
|
// A protocol-6 frame: a BuildingBlock with an owner and no `authorized`.
|
|
await emit.onEvent(SERVER, { kind: 'entity.destroyed', frame: { kind: 'entity.destroyed', t: NOW, ownerId: '100', prefab: 'wall' } })
|
|
assert.strictEqual(calls.length, 0, 'the placer is never a fallback')
|
|
} finally {
|
|
restore()
|
|
}
|
|
})
|
|
|
|
test('a replayed raid is still told for a day, and not after (D63)', async () => {
|
|
const { emit, calls, restore } = setup({ links: { 101: 11 } })
|
|
try {
|
|
await emit.onEvent(SERVER, { kind: 'entity.destroyed', frame: raidFrame({ t: NOW - 3 * 3600 * 1000 }) })
|
|
assert.strictEqual(calls.length, 1)
|
|
await emit.onEvent(SERVER, { kind: 'entity.destroyed', frame: raidFrame({ t: NOW - 25 * 3600 * 1000 }) })
|
|
assert.strictEqual(calls.length, 1)
|
|
} finally {
|
|
restore()
|
|
}
|
|
})
|
|
|
|
test('the same frame replayed carries the same dedupe key, within core\'s bound', async () => {
|
|
const { emit, calls, restore } = setup({ links: { 101: 11 } })
|
|
try {
|
|
const item = { id: 7, kind: 'entity.destroyed', frame: raidFrame() }
|
|
await emit.onEvent(SERVER, item)
|
|
await emit.onEvent(SERVER, { ...item, id: 99 })
|
|
assert.strictEqual(calls[0][1].dedupeKey, calls[1][1].dedupeKey, 'keyed on the event, not the row id')
|
|
assert.ok(calls[0][1].dedupeKey.length <= 190)
|
|
} finally {
|
|
restore()
|
|
}
|
|
})
|
|
|
|
// ── Broadcasts ─────────────────────────────────────────────────────────────
|
|
|
|
test('a wipe is news for fifteen minutes (D63)', async () => {
|
|
const { emit, calls, restore } = setup()
|
|
try {
|
|
await emit.onEvent(SERVER, { kind: 'server.wipe', frame: { kind: 'server.wipe', t: NOW - 60 * 1000, wipeId: 'w2' } })
|
|
await emit.onEvent(SERVER, { kind: 'server.wipe', frame: { kind: 'server.wipe', t: NOW - 20 * 60 * 1000, wipeId: 'w3' } })
|
|
assert.strictEqual(calls.length, 1)
|
|
assert.strictEqual(calls[0][0], 'rust.wipe.started')
|
|
assert.strictEqual(calls[0][1].data.wipeId, 'w2')
|
|
} finally {
|
|
restore()
|
|
}
|
|
})
|
|
|
|
test('online and offline are transitions, and a first sighting is not one', () => {
|
|
const { emit, calls, restore } = setup()
|
|
try {
|
|
emit.serverObserved(SERVER, true)
|
|
emit.serverObserved(SERVER, true)
|
|
assert.strictEqual(calls.length, 0, 'a restart announces nothing')
|
|
emit.serverObserved(SERVER, false)
|
|
emit.serverObserved(SERVER, false)
|
|
emit.serverObserved(SERVER, true)
|
|
assert.deepStrictEqual(calls.map((c) => c[0]), ['rust.server.offline', 'rust.server.online'])
|
|
assert.strictEqual(calls[0][1].data.serverId, 'main')
|
|
} finally {
|
|
restore()
|
|
}
|
|
})
|
|
|
|
test('a new leader is announced once; a tie is not a new leader', async () => {
|
|
const row = (steamId, kills) => ({ steamId, name: `P${steamId}`, kills })
|
|
const { emit, calls, restore } = setup({
|
|
state: { wipeId: 'w1' },
|
|
board: [
|
|
[row('1', 5), row('2', 3)], // first sight: remembered, not announced
|
|
[row('2', 5), row('1', 5)], // level on kills: not a change
|
|
[row('2', 6), row('1', 5)], // strictly ahead: announced
|
|
[row('2', 7), row('1', 5)], // the same leader: nothing
|
|
],
|
|
})
|
|
try {
|
|
for (let i = 0; i < 4; i += 1) await emit.checkLeader(SERVER)
|
|
assert.strictEqual(calls.length, 1)
|
|
assert.strictEqual(calls[0][0], 'rust.leaderboard.topped')
|
|
assert.strictEqual(calls[0][1].data.leader, 'P2')
|
|
assert.strictEqual(calls[0][1].data.kills, 6)
|
|
} finally {
|
|
restore()
|
|
}
|
|
})
|
|
|
|
// ── Clans ──────────────────────────────────────────────────────────────────
|
|
|
|
test('a disband goes to the roster on the frame, not to the one who did it', async () => {
|
|
const { emit, calls, restore } = setup({ links: { 201: 21, 202: 22, 203: 23 } })
|
|
try {
|
|
await emit.onEvent(SERVER, {
|
|
kind: 'clan.disbanded',
|
|
frame: { kind: 'clan.disbanded', t: NOW, clanId: 4, clanName: 'Belt', steamId: '201', name: 'Boss', members: ['201', '202', '203'] },
|
|
})
|
|
assert.strictEqual(calls.length, 1)
|
|
assert.strictEqual(calls[0][0], 'rust.clan.disbanded')
|
|
assert.deepStrictEqual(calls[0][1].recipientUserIds.sort(), [22, 23])
|
|
assert.strictEqual(calls[0][1].data.by, 'Boss')
|
|
assert.match(calls[0][1].data.clanUrl, RELATIVE_URL)
|
|
} finally {
|
|
restore()
|
|
}
|
|
})
|
|
|
|
test('the one kicked is told; the one who kicked is not', async () => {
|
|
const { emit, calls, restore } = setup({
|
|
links: { 301: 31, 302: 32, 303: 33 },
|
|
members: { 'main:5:1': ['301', '302'] }, // the board already dropped 303
|
|
})
|
|
try {
|
|
await emit.onEvent(SERVER, {
|
|
kind: 'clan.member.kicked',
|
|
frame: { kind: 'clan.member.kicked', t: NOW, clanId: 5, steamId: '303', name: 'Out', bySteamId: '301', byName: 'Boss' },
|
|
})
|
|
assert.deepStrictEqual(calls[0][1].recipientUserIds.sort(), [32, 33])
|
|
} finally {
|
|
restore()
|
|
}
|
|
})
|
|
|
|
test('a leaver is not told they left, and a lone leaver tells nobody', async () => {
|
|
const { emit, calls, restore } = setup({ links: { 401: 41, 402: 42 }, members: { 'main:6:1': ['401', '402'] } })
|
|
try {
|
|
await emit.onEvent(SERVER, { kind: 'clan.member.left', frame: { kind: 'clan.member.left', t: NOW, clanId: 6, steamId: '402' } })
|
|
assert.deepStrictEqual(calls[0][1].recipientUserIds, [41])
|
|
await emit.onEvent(SERVER, { kind: 'clan.member.left', frame: { kind: 'clan.member.left', t: NOW, clanId: 7, steamId: '402' } })
|
|
assert.strictEqual(calls.length, 1)
|
|
} finally {
|
|
restore()
|
|
}
|
|
})
|
|
|
|
// ── Moderation ─────────────────────────────────────────────────────────────
|
|
|
|
test('a ban never carries the address the frame does', async () => {
|
|
const { emit, calls, restore } = setup()
|
|
try {
|
|
await emit.onEvent(SERVER, {
|
|
kind: 'player.banned',
|
|
frame: { kind: 'player.banned', t: NOW, steamId: '555', name: 'Cheater', ip: '203.0.113.9', reason: 'aimbot' },
|
|
})
|
|
assert.strictEqual(calls.length, 1)
|
|
assert.ok(!JSON.stringify(calls[0][1]).includes('203.0.113.9'))
|
|
assert.strictEqual(calls[0][1].data.reason, 'aimbot')
|
|
} finally {
|
|
restore()
|
|
}
|
|
})
|
|
|
|
test('an unapproved login becomes a staff notice, keyed on the attempt', async () => {
|
|
const { emit, calls, restore, eventsDb } = setup()
|
|
try {
|
|
const asked = []
|
|
eventsDb.unapprovedLogins = async (q) => {
|
|
asked.push(q)
|
|
return [{ steamId: '777', t: NOW - 120000, name: 'Knocker' }]
|
|
}
|
|
const sent = await emit.sweepLoginDenied([SERVER], NOW)
|
|
await emit.sweepLoginDenied([SERVER], NOW)
|
|
assert.strictEqual(sent, 1)
|
|
assert.strictEqual(asked[0].to, NOW - emit.LOGIN_APPROVAL_WINDOW_MS, 'an attempt waits its minute first')
|
|
assert.strictEqual(calls[0][0], 'rust.login.denied')
|
|
assert.strictEqual(calls[0][1].dedupeKey, calls[1][1].dedupeKey, 'a second sweep is a no-op in core')
|
|
} finally {
|
|
restore()
|
|
}
|
|
})
|
|
|
|
// ── The rest ───────────────────────────────────────────────────────────────
|
|
|
|
test('a new link tells its owner, and only its owner', () => {
|
|
const { emit, calls, restore } = setup()
|
|
try {
|
|
emit.linked({ userId: 5, steamId: '76561198000000001', name: 'Me' })
|
|
assert.strictEqual(calls[0][0], 'rust.player.linked')
|
|
assert.strictEqual(calls[0][1].ownerUserId, 5)
|
|
assert.strictEqual(emit.linked({ userId: 0, steamId: 'x' }), 0)
|
|
} finally {
|
|
restore()
|
|
}
|
|
})
|
|
|
|
test('a frame the fan-out cannot handle costs one notice, never the caller', async () => {
|
|
const { emit, restore } = setup()
|
|
try {
|
|
const linksDb = require('../model/links/links.db')
|
|
linksDb.userIdsForSteamIds = async () => { throw new Error('database gone') }
|
|
const sent = await emit.onEvent(SERVER, { kind: 'entity.destroyed', frame: raidFrame() })
|
|
assert.strictEqual(sent, 0)
|
|
} finally {
|
|
restore()
|
|
}
|
|
})
|
|
|
|
test('an audience that fails answers nobody, never everybody', async () => {
|
|
require('../core')._reset()
|
|
const ctx = fakeCtx({ db: { query: spy(() => Promise.reject(new Error('down'))), pool: {} } })
|
|
require('../core').init(ctx)
|
|
const { AUDIENCES } = require('../engagement/audiences')
|
|
for (const a of AUDIENCES) {
|
|
assert.deepStrictEqual(await a.resolve({ clan: 'main:1:1', serverId: 'main' }), [], a.id)
|
|
assert.deepStrictEqual(await a.resolve({}), [], `${a.id} with no param`)
|
|
}
|
|
})
|