// ── The leases (PLAN.md §27, protocol 8) ────────────────────────────────── // // Core owns the lease: it reads the baseline, reserves the target, applies the // value and restores it. What this module owns is four callables per lease, and // every test here is one of the ways those can be subtly wrong while looking // right: // // the target must name the server, or two servers share one holder (D73) // a duration crosses the wire, not a deadline, or two clocks disagree // drift is an ANSWER, not a failure of the call // "still held" is read from the holds, never inferred from a changed value // a key already held reads as its BASELINE, or teardown restores the event // an apply this end gave up on is followed by a release // two lease calls fit inside core.lease's budget, asserted not trusted 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 servers = require('../model/servers/servers.model') const { LEASES, OPTION_SOURCES, MAX_LEASE_MS, splitTarget } = require('../eventLeases') const byId = (id) => LEASES.find((l) => l.id === id) const source = (id) => OPTION_SOURCES.find((s) => s.id === id) const ROWS = { main: { id: 'main', name: 'Main', sidecarBaseUrl: 'http://main:1', sidecarTokenEnc: null, enabled: 1 }, off: { id: 'off', name: 'Off', sidecarBaseUrl: 'http://off:1', sidecarTokenEnc: null, enabled: 0 }, } /** Replace the module's collaborators for one test, and put them back after. */ function stub(t, { list, apply, release, catalogue, polling } = {}) { const calls = { list: [], apply: [], release: [], catalogue: [] } const saved = { getServer: serversDb.getServer, listForPolling: servers.listForPolling, leaseList: client.leaseList, leaseApply: client.leaseApply, leaseRelease: client.leaseRelease, permCatalogue: client.permCatalogue, } serversDb.getServer = async (id) => ROWS[id] || null servers.listForPolling = async () => (polling || ['main']).map((id) => ({ id, name: ROWS[id] ? ROWS[id].name : id, baseUrl: `http://${id}:1`, token: 't' })) client.leaseList = async (server, q) => { calls.list.push({ server: server.id, ...q }) return list ? list(server, q) : { ok: false, status: 'http-503' } } client.leaseApply = async (server, body) => { calls.apply.push({ server: server.id, body }) return apply ? apply(server, body) : { ok: true, data: { kind: 'lease.ok' } } } client.leaseRelease = async (server, body) => { calls.release.push({ server: server.id, body }) return release ? release(server, body) : { ok: true, data: { kind: 'lease.ok' } } } client.permCatalogue = async (server) => { calls.catalogue.push(server.id) return catalogue ? catalogue(server) : { ok: false, status: 'http-503' } } t.after(() => { serversDb.getServer = saved.getServer servers.listForPolling = saved.listForPolling client.leaseList = saved.leaseList client.leaseApply = saved.leaseApply client.leaseRelease = saved.leaseRelease client.permCatalogue = saved.permCatalogue }) return calls } test('two lease calls fit inside the budget core.lease runs under', () => { // `core.lease` declares no budgetMs, so the dispatcher's 10s default applies, // and it spends it on read() THEN apply(). At the client's ordinary 12s either // one alone would outlast the step, and the dispatcher would call a retry // while this module was still waiting — the ordering sidecarClient's header // exists to forbid. assert.ok(2 * client.LEASE_TIMEOUT_MS < client.CORE_LEASE_BUDGET_MS) assert.ok(client.LEASE_TIMEOUT_MS < client.TIMEOUT_MS) }) test('every lease may be held for seven days and no longer (D77)', () => { assert.strictEqual(MAX_LEASE_MS, 7 * 24 * 60 * 60 * 1000) for (const l of LEASES) assert.strictEqual(l.maxDurationMs, MAX_LEASE_MS, l.id) }) test('a target splits at the FIRST slash, so a group name may contain one', () => { assert.deepStrictEqual(splitTarget('main'), { serverId: 'main', rest: '' }) assert.deepStrictEqual(splitTarget('main/bear.population'), { serverId: 'main', rest: 'bear.population' }) assert.deepStrictEqual(splitTarget('main/a/b/kits.vip'), { serverId: 'main', rest: 'a/b/kits.vip' }) }) test('a target naming no server, or a switched-off one, is refused for good', async (t) => { stub(t) const decay = byId('rust.decay.scale') for (const target of ['', 'nowhere', 'off']) { const answer = await decay.read({ target }) assert.strictEqual(answer.ok, false, target) assert.strictEqual(answer.retry, false, `${target}: the second attempt carries the same params`) } }) test('the decay rate takes only a server, and a population needs one named', async (t) => { stub(t) const decay = await byId('rust.decay.scale').read({ target: 'main/bear.population' }) assert.strictEqual(decay.ok, false) assert.strictEqual(decay.retry, false) const pop = await byId('rust.population').read({ target: 'main' }) assert.strictEqual(pop.ok, false) assert.strictEqual(pop.retry, false) }) test('read asks the plugin about one key on the named server, and answers the current value as text', async (t) => { const calls = stub(t, { list: () => ({ ok: true, data: { leases: [{ key: 'bear.population', family: 'population', current: '2', held: false }], holds: [] } }), }) const answer = await byId('rust.population').read({ target: 'main/Bear.Population' }) assert.deepStrictEqual(answer, { ok: true, value: '2' }) assert.deepStrictEqual(calls.list, [{ server: 'main', key: 'bear.population', target: undefined }]) }) test('a key from another family is refused even when the plugin lends it', async (t) => { stub(t, { list: () => ({ ok: true, data: { leases: [{ key: 'spawn.max_rate', family: 'spawn', current: '1' }] } }), }) const answer = await byId('rust.population').read({ target: 'main/spawn.max_rate' }) assert.strictEqual(answer.ok, false) assert.strictEqual(answer.retry, false) }) test('a key the plugin already holds reads as its BASELINE, not the value the lease put there', async (t) => { // Core's reservation stops a second run reaching read(). A hold core does not // know about is this run's first attempt with its answer lost, and the value // to give back at the end is what was there before anybody borrowed it. stub(t, { list: () => ({ ok: true, data: { leases: [{ key: 'decay.scale', family: 'decay', current: '0', held: true, baseline: '1', applied: '0' }] }, }), }) const answer = await byId('rust.decay.scale').read({ target: 'main' }) assert.deepStrictEqual(answer, { ok: true, value: '1' }) }) test('apply sends a DURATION, the family, and the value as text', async (t) => { const calls = stub(t) const until = new Date(Date.now() + 60 * 60 * 1000) const answer = await byId('rust.population').apply(4, until, { target: 'main/bear.population' }) assert.deepStrictEqual(answer, { ok: true }) const { body } = calls.apply[0] assert.strictEqual(body.key, 'bear.population') assert.strictEqual(body.family, 'population') assert.strictEqual(body.value, '4') assert.strictEqual(body.untilMs, until.getTime()) // holdMs is what the plugin arms its timer with; it must be the remaining // duration, measured here, and never the absolute time. assert.ok(body.holdMs > 59 * 60 * 1000 && body.holdMs <= 60 * 60 * 1000, String(body.holdMs)) }) test('a group permission crosses as a key and a lowered group/permission target', async (t) => { const calls = stub(t) await byId('rust.group.permission').apply(true, new Date(Date.now() + 60000), { target: 'main/Default/Kits.VIP' }) const { body } = calls.apply[0] assert.strictEqual(body.key, 'group.permission') assert.strictEqual(body.target, 'default/kits.vip') assert.strictEqual(body.value, 'true') assert.strictEqual(body.family, undefined) }) test('events switched off on the server is a refusal with the switch named, for good (D76)', async (t) => { stub(t, { apply: () => ({ ok: true, data: { kind: 'lease.error', reason: 'events-disabled', message: 'events are switched off on this server — set EventsEnabled' }, }), }) const answer = await byId('rust.decay.scale').apply(0, new Date(Date.now() + 60000), { target: 'main' }) assert.strictEqual(answer.ok, false) assert.strictEqual(answer.retry, false) assert.match(answer.error, /EventsEnabled/) }) test('an apply this end gave up on is followed by a release of the same value', async (t) => { const calls = stub(t, { apply: () => ({ ok: false, status: 'timeout' }) }) const answer = await byId('rust.decay.scale').apply(0, new Date(Date.now() + 60000), { target: 'main' }) assert.strictEqual(answer.ok, false) assert.match(answer.error, /Main/, 'every notice says which server') await new Promise((resolve) => setImmediate(resolve)) assert.strictEqual(calls.release.length, 1) assert.deepStrictEqual(calls.release[0].body, { key: 'decay.scale', target: undefined, expected: '0' }) }) test('an apply that failed for any other reason sends no release', async (t) => { const calls = stub(t, { apply: () => ({ ok: false, status: 'http-503' }) }) await byId('rust.decay.scale').apply(0, new Date(Date.now() + 60000), { target: 'main' }) await new Promise((resolve) => setImmediate(resolve)) assert.strictEqual(calls.release.length, 0) }) test('an apply whose deadline has already passed is refused before anything is sent', async (t) => { const calls = stub(t) const answer = await byId('rust.decay.scale').apply(0, new Date(Date.now() - 1000), { target: 'main' }) assert.strictEqual(answer.ok, false) assert.strictEqual(calls.apply.length, 0) }) test('drift is an answer with the current value beside it, not a failed call', async (t) => { const calls = stub(t, { release: () => ({ ok: true, data: { kind: 'lease.drifted', current: '3' } }) }) const answer = await byId('rust.decay.scale').restore(1, { expected: 0, target: 'main' }) assert.deepStrictEqual(answer, { ok: false, drifted: true, current: '3' }) assert.deepStrictEqual(calls.release[0].body, { key: 'decay.scale', expected: '0', baseline: '1' }) }) test('a release the plugin accepted — including a group that is gone — is a success', async (t) => { stub(t, { release: () => ({ ok: true, data: { kind: 'lease.ok', targetGone: true } }) }) const answer = await byId('rust.group.permission').restore(false, { expected: true, target: 'main/vip/kits.vip' }) assert.deepStrictEqual(answer, { ok: true }) }) test('a release that could not be made is a failure core will ask again about', async (t) => { stub(t, { release: () => ({ ok: false, status: 'http-503' }) }) const answer = await byId('rust.decay.scale').restore(1, { expected: 0, target: 'main' }) assert.strictEqual(answer.ok, false) assert.strictEqual(answer.drifted, undefined) assert.strictEqual(answer.retry, undefined) }) test('"still held" is read from the holds, never from a value that changed', async (t) => { // The current value differs from anything a lease applied — somebody moved it. // That is DRIFT, for restore() to report; inForce must still say the hold is // there, or core orphans the row before restore ever gets to say so. stub(t, { list: () => ({ ok: true, data: { leases: [{ key: 'decay.scale', family: 'decay', current: '7', held: true }], holds: [{ key: 'decay.scale', applied: '0', baseline: '1' }], }, }), }) assert.deepStrictEqual(await byId('rust.decay.scale').inForce({ target: 'main' }), { ok: true, held: true }) }) test('after a restart the plugin holds nothing, and inForce says so', async (t) => { stub(t, { list: () => ({ ok: true, data: { leases: [{ key: 'decay.scale', family: 'decay', current: '1' }], holds: [] } }) }) assert.deepStrictEqual(await byId('rust.decay.scale').inForce({ target: 'main' }), { ok: true, held: false }) }) test('inForce that cannot reach the game is not an answer', async (t) => { stub(t) const answer = await byId('rust.decay.scale').inForce({ target: 'main' }) assert.strictEqual(answer.ok, false) }) test('a group permission hold is matched on its own target', async (t) => { stub(t, { list: () => ({ ok: true, data: { holds: [{ key: 'group.permission', target: 'default/kits.vip' }] } }), }) const lease = byId('rust.group.permission') assert.deepStrictEqual(await lease.inForce({ target: 'main/default/kits.vip' }), { ok: true, held: true }) assert.deepStrictEqual(await lease.inForce({ target: 'main/default/kits.gold' }), { ok: true, held: false }) }) test('the servers source is every enabled server, with no game call', async (t) => { const calls = stub(t, { polling: ['main', 'creative'] }) const rows = await source('rust.options.servers').resolve() assert.deepStrictEqual(rows.map((r) => r.value), ['main', 'creative']) assert.strictEqual(calls.list.length, 0) }) test('a family source lists whole targets, and one silent server blanks nothing', async (t) => { stub(t, { polling: ['main', 'creative'], list: (server) => server.id === 'creative' ? { ok: false, status: 'timeout' } : { ok: true, data: { leases: [ { key: 'decay.scale', family: 'decay', current: '1' }, { key: 'bear.population', family: 'population', current: '2' }, { key: 'zombie.population', family: 'population', unreadable: 'this server has no convar' }, ], }, }, }) const rows = await source('rust.options.populations').resolve() assert.deepStrictEqual(rows, [{ value: 'main/bear.population', label: 'bear.population', group: 'Main' }]) }) test('the group-permission source is searchable and narrows by the term', async (t) => { stub(t, { catalogue: () => ({ ok: true, data: { permissions: ['kits.vip', 'Kits.Gold', 'zonemanager.admin'], groups: [{ name: 'default' }, { name: 'VIP' }] }, }), }) const src = source('rust.options.grouppermissions') assert.strictEqual(src.searchable, true) const all = await src.resolve({ q: '' }) assert.strictEqual(all.length, 6) const narrowed = await src.resolve({ q: 'kits' }) assert.deepStrictEqual( narrowed.map((r) => r.value).sort(), ['main/default/kits.gold', 'main/default/kits.vip', 'main/vip/kits.gold', 'main/vip/kits.vip'], ) })