// ── Zones: presets, options and the dome (PLAN_REDESIGNS §3, D210, D211) ───
//
// What each test guards:
//
// a step's options line reads back as the flags and settings it names, spelled
// as the server's own ZoneManager spells them (D211)
// a flag the server does not have is refused on the form, not at 4 a.m.
// a flag this module does not know is drawn under Other, never hidden
// a preset is checked against every server it covers, and one that has not
// said is left to the plugin
// two presets of one name may not share a server; across servers they may (D189)
// the step's dropdown VALUE is the line itself, so picking copies it (D210)
// the zone crosses with its flags, settings, messages and dome
// a dome on a server without ZoneDomes or its helper is refused with the reason
const test = require('node:test')
const assert = require('node:assert')
const { fakeCtx } = require('./_fakes')
require('../core')._reset()
require('../core').init(fakeCtx())
const client = require('../sidecarClient')
const serversDb = require('../model/servers/servers.db')
const zonesDb = require('../model/zones/zones.db')
const zones = require('../model/zones/zones.model')
const options = require('../model/zones/zoneOptions')
const voice = require('../model/permissions/voice')
const world = require('../eventWorld')
const action = (id) => world.ACTIONS.find((a) => a.id === id)
const source = (id) => world.OPTION_SOURCES.find((s) => s.id === id)
const FLAGS = ['AutoLights', 'NoBuild', 'NoPlayerLoot', 'PvpGod', 'NoChat', 'SomethingNew']
function server(id, { flags = FLAGS, domes = null } = {}) {
return {
id,
name: id.toUpperCase(),
enabled: true,
zoneManager: flags ? { version: '3.1.14', flags } : null,
zoneDomes: domes,
}
}
/** Stubs the preset store and the servers' hellos for one test. */
function stubZones(t, { servers = [server('main')], presets = [] } = {}) {
const saved = { ...zonesDb }
const store = presets.map((p) => ({ ...p }))
const writes = []
zonesDb.listZoneServers = async () => servers
zonesDb.listPresets = async () => store.map((p) => ({ ...p }))
zonesDb.getPreset = async (id) => store.find((p) => p.id === id) || null
zonesDb.savePreset = async (row) => {
writes.push(row)
const id = row.id || store.length + 100
const next = { id, ...row, servers: row.allServers ? [] : row.servers, updatedAt: null }
const at = store.findIndex((p) => p.id === id)
if (at >= 0) store[at] = next
else store.push(next)
return id
}
zonesDb.deletePreset = async (id) => {
const at = store.findIndex((p) => p.id === id)
if (at >= 0) store.splice(at, 1)
}
t.after(() => Object.assign(zonesDb, saved))
return { store, writes }
}
function stubWorld(t) {
const calls = []
const saved = { getServer: serversDb.getServer, worldZone: client.worldZone, currentFormat: voice.currentFormat }
serversDb.getServer = async (id) => ({ id, name: id, sidecarBaseUrl: `http://${id}:1`, sidecarTokenEnc: null, enabled: 1 })
client.worldZone = async (srv, body) => {
calls.push({ server: srv.id, body })
return { ok: true, status: 'ok', data: { kind: 'world.ok', placed: [{ id: 'rg-7-1-1', kind: 'zone', name: 'Z' }] } }
}
voice.currentFormat = async () => '[Event] {message}'
t.after(() => {
serversDb.getServer = saved.getServer
client.worldZone = saved.worldZone
voice.currentFormat = saved.currentFormat
})
return calls
}
// ── the options line ───────────────────────────────────────────────────────
test('an options line reads as flags and settings, spelled as the server spells them', () => {
const read = options.parse('nobuild, NOPLAYERLOOT radiation=10 safezone=true permission=Arena_1', FLAGS)
assert.deepStrictEqual(read, {
ok: true,
flags: ['NoBuild', 'NoPlayerLoot'],
settings: { radiation: 10, safezone: true, permission: 'arena_1' },
})
assert.deepStrictEqual(options.parse('', FLAGS), { ok: true, flags: [], settings: {} })
assert.deepStrictEqual(options.parse(undefined), { ok: true, flags: [], settings: {} })
})
test('a flag the server lacks, or a setting out of range, is refused', () => {
for (const line of [
'NoFlying',
'radiation=501',
'comfort=2',
'temperature=-101',
'safezone=maybe',
'permission=has.dots',
'rotation=90',
'radiation=1 radiation=2',
'No-Build',
]) {
const read = options.parse(line, FLAGS)
assert.strictEqual(read.ok, false, line)
assert.ok(read.error, line)
}
})
test('without a flag list the flags are kept as written, and the plugin checks them', () => {
assert.deepStrictEqual(options.parse('NoFlying', null), { ok: true, flags: ['NoFlying'], settings: {} })
})
test('format and parse are inverses', () => {
const preset = { flags: ['NoBuild', 'PvpGod'], settings: { safezone: false, radiation: 2.5 } }
const line = options.format(preset)
assert.strictEqual(line, 'NoBuild, PvpGod, radiation=2.5, safezone=false')
const back = options.parse(line, FLAGS)
assert.deepStrictEqual({ flags: back.flags, settings: back.settings }, preset)
})
test('a flag this module does not know is drawn under Other, never hidden', () => {
const groups = options.grouped(FLAGS)
assert.deepStrictEqual(groups.map((g) => g.group), ['Combat', 'Building', 'Loot', 'Communication', 'Other'])
assert.deepStrictEqual(groups.find((g) => g.group === 'Other').flags.map((f) => f.name), ['SomethingNew'])
assert.strictEqual(groups.flatMap((g) => g.flags).length, FLAGS.length)
})
// ── presets ────────────────────────────────────────────────────────────────
test('a preset is checked against every server it covers, and saved as the server spells it', async (t) => {
const { writes } = stubZones(t, {
servers: [server('main'), server('alt', { flags: ['NoBuild'] }), server('dark', { flags: null })],
})
await assert.rejects(zones.create({ name: 'Arena', flags: ['NoPlayerLoot'], servers: ['main', 'alt'] }), (err) => {
assert.strictEqual(err.status, 400)
assert.match(err.message, /ALT/)
return true
})
// `dark` has not said which flags it has: skipped, and the plugin checks.
const saved = await zones.create({ name: 'Arena', flags: ['noplayerloot'], settings: { radiation: '5' }, servers: ['main', 'dark'] }, 9)
assert.deepStrictEqual(writes[0], {
name: 'Arena', flags: ['NoPlayerLoot'], settings: { radiation: 5 }, allServers: false, servers: ['main', 'dark'],
})
assert.strictEqual(saved.name, 'Arena')
})
test('a preset names a server, or every server, and only ones that exist', async (t) => {
stubZones(t)
await assert.rejects(zones.create({ name: 'A', flags: [] }), /at least one server/)
await assert.rejects(zones.create({ name: 'A', servers: ['nowhere'] }), (err) => err.status === 404)
await assert.rejects(zones.create({ name: '', servers: ['main'] }), /name/)
})
test('two presets of one name may not share a server; on different servers they may (D189)', async (t) => {
stubZones(t, {
servers: [server('main'), server('alt')],
presets: [{ id: 1, name: 'Arena', flags: [], settings: {}, allServers: false, servers: ['main'] }],
})
await assert.rejects(zones.create({ name: 'arena', servers: ['main', 'alt'] }), (err) => err.status === 409)
await assert.rejects(zones.create({ name: 'Arena', allServers: true }), (err) => err.status === 409)
const alt = await zones.create({ name: 'Arena', servers: ['alt'] })
assert.deepStrictEqual(alt.servers, ['alt'])
// Editing the first keeps its own name without clashing with itself.
const same = await zones.update(1, { name: 'Arena', flags: ['PvpGod'], servers: ['main'] })
assert.deepStrictEqual(same.flags, ['PvpGod'])
})
test('the zone step’s dropdown VALUE is the options line, so picking a preset copies it (D210)', async (t) => {
stubZones(t, {
servers: [server('main')],
presets: [
{ id: 1, name: 'Arena', flags: ['NoBuild'], settings: { radiation: 10 }, allServers: false, servers: ['main'] },
{ id: 2, name: 'Calm', flags: ['PvpGod'], settings: {}, allServers: true, servers: [] },
],
})
assert.deepStrictEqual(await source('rust.options.zone_presets').resolve({}), [
{ value: 'NoBuild, radiation=10', label: 'Arena', group: 'MAIN' },
{ value: 'PvpGod', label: 'Calm', group: 'Every server' },
])
})
// ── the step ───────────────────────────────────────────────────────────────
const PATCHED = { loaded: true, version: '2.0.2', helper: { state: 'patched', version: '0.1.0' }, ready: true }
test('a zone crosses with its flags, settings, messages in the chat voice, and its dome', async (t) => {
stubZones(t, { servers: [server('main', { domes: PATCHED })] })
const calls = stubWorld(t)
const result = await action('rust.zone.open').perform({
runId: 7,
idempotencyKey: 'k1',
params: {
monument: 'main/airfield_1', radius: 40, minutes: 30,
options: 'nobuild, pvpgod, radiation=10',
enterMessage: ' In you go. ', leaveMessage: 'Out.',
dome: 'Red', domeStack: 2,
},
})
assert.strictEqual(result.ok, true, result.error)
assert.deepStrictEqual(calls[0].body, {
runId: '7', key: 'k1', monument: 'airfield_1', offsetX: 0, offsetZ: 0, radius: 40, holdMs: 1800000,
flags: ['NoBuild', 'PvpGod'],
settings: { radiation: 10 },
enterMessage: 'In you go.',
leaveMessage: 'Out.',
delivery: 'chat',
format: '[Event] {message}',
dome: { type: 1, stack: 2 },
})
})
test('a zone with no options crosses exactly as it always did', async (t) => {
stubZones(t)
const calls = stubWorld(t)
await action('rust.zone.open').perform({ runId: 7, idempotencyKey: 'k', params: { monument: 'main/a', radius: 40, minutes: 5 } })
assert.deepStrictEqual(Object.keys(calls[0].body).sort(), ['holdMs', 'key', 'monument', 'offsetX', 'offsetZ', 'radius', 'runId'])
})
test('a popup carries no chat voice', async (t) => {
stubZones(t)
const calls = stubWorld(t)
await action('rust.zone.open').perform({
runId: 7, idempotencyKey: 'k', params: { monument: 'main/a', radius: 40, minutes: 5, enterMessage: 'Hi', delivery: 'popup' },
})
assert.strictEqual(calls[0].body.delivery, 'popup')
assert.strictEqual(calls[0].body.format, undefined)
})
test('a bad option, message or dome is refused for good, before anything is sent', async (t) => {
const run = (params) =>
action('rust.zone.open').perform({ runId: 7, idempotencyKey: 'k', params: { monument: 'main/a', radius: 40, minutes: 5, ...params } })
for (const [domes, params, words] of [
[PATCHED, { options: 'NoFlying' }, /not a flag/],
[PATCHED, { enterMessage: 'x'.repeat(257) }, /256/],
[PATCHED, { delivery: 'shout' }, /chat or to a popup/],
[PATCHED, { dome: 'orange' }, /standard, red/],
[PATCHED, { dome: 'red', domeStack: 11 }, /1 to 10/],
[{ loaded: false }, { dome: 'red' }, /ZoneDomes is not loaded/],
[{ loaded: true, helper: { state: 'missing' } }, { dome: 'red' }, /RunicGatewayDomes\.cs is not installed/],
[{ loaded: true, helper: { state: 'unsupported' } }, { dome: 'red' }, /could not patch/],
]) {
await t.test(String(words), async (tt) => {
stubZones(tt, { servers: [server('main', { domes })] })
const calls = stubWorld(tt)
const result = await run(params)
assert.strictEqual(result.ok, false)
assert.strictEqual(result.retry, false)
assert.match(result.error, words)
assert.strictEqual(calls.length, 0)
})
}
})
test('a dry run checks the options and the dome and sends nothing', async (t) => {
stubZones(t, { servers: [server('main', { domes: PATCHED })] })
const calls = stubWorld(t)
const result = await action('rust.zone.open').perform({
runId: 7, idempotencyKey: 'k', verify: true, params: { monument: 'main/a', radius: 40, minutes: 5, options: 'NoBuild', dome: 'standard' },
})
assert.deepStrictEqual(result, { ok: true })
assert.strictEqual(calls.length, 0)
})
test('the dome dropdown offers ZoneDomes’ five colours', async () => {
assert.deepStrictEqual((await source('rust.options.domes').resolve()).map((r) => r.value), ['standard', 'red', 'blue', 'green', 'purple'])
})