// ── 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']) })