// ── Backpacks (PLAN_REDESIGNS §11.3.2, protocol 14, D337, D338, D340, D341) ─ // // Each test is one way the feature could look right and be wrong: // // the sizes a step may lease skip the smallest, which everybody holds // a server without Backpacks is refused with the reason, in the dry run too (D199) // a size the server does not offer is refused by name // the lease is a kit reward's row: the size's permission, no kit, no credit (D337) // an empty message is the take-back warning, never silence (D338) // the revert deletes the step's rows and marks the server for a push // a read answers what the game said, live; a stranger's id never reaches it // a stack keeps its name, blueprint, skin as text, and one level of contents const test = require('node:test') const assert = require('node:assert') const { fakeCtx } = require('./_fakes') function withRecorder(rows = []) { const statements = [] const ctx = fakeCtx({ db: { query: (sql, params = []) => { statements.push({ sql, params }) return Promise.resolve(typeof rows === 'function' ? rows(sql, params) : rows) }, pool: {}, }, }) require('../core')._reset() require('../core').init(ctx) return { statements } } withRecorder() const client = require('../sidecarClient') const serversDb = require('../model/servers/servers.db') const servers = require('../model/servers/servers.model') const linksDb = require('../model/links/links.db') const permDb = require('../model/permissions/permissions.db') const backpacksDb = require('../model/backpacks/backpacks.db') const backpacks = require('../model/backpacks/backpacks.model') const rewards = require('../eventRewards') const ok = (data) => ({ ok: true, status: 'ok', data }) const A = '76561198000000001' const B = '76561198000000002' const C = '76561198000000003' const SIZES = [6, 12, 18, 24, 30, 36, 42, 48, 96, 144] // ── What a server reports ────────────────────────────────────────────────── test('a server’s sizes are whole, ascending and unique; the smallest is never leased', () => { assert.deepStrictEqual(backpacks.shape(null), { loaded: false, sizes: [] }) assert.deepStrictEqual(backpacks.shape(JSON.stringify({ loaded: false })), { loaded: false, sizes: [] }) const report = backpacks.shape(JSON.stringify({ loaded: true, version: '3.17.8', sizes: [48, 6, 12, 12, 'x', -3, 2.5] })) assert.deepStrictEqual(report, { loaded: true, version: '3.17.8', sizes: [6, 12, 48] }) assert.deepStrictEqual(backpacks.leasable(report), [12, 48]) assert.deepStrictEqual(backpacks.leasable({ loaded: false, sizes: [] }), []) }) // ── The step ─────────────────────────────────────────────────────────────── const ROW = { id: 'main', name: 'Main', sidecarBaseUrl: 'http://main:1', sidecarTokenEnc: null, enabled: 1 } const lend = rewards.ACTIONS.find((a) => a.id === 'rust.backpack.size') function stub(t, { loaded = true, sizes = SIZES, people = [], links = {}, existing = [] } = {}) { const calls = { inserted: [], dirty: [], deleted: [] } const saved = { getServer: serversDb.getServer, reportFor: backpacksDb.reportFor, tallySnapshot: client.tallySnapshot, userIdsForSteamIds: linksDb.userIdsForSteamIds, listRunGrantsForStep: permDb.listRunGrantsForStep, insertRunGrants: permDb.insertRunGrants, markDirty: permDb.markDirty, deleteRunGrantsForStep: permDb.deleteRunGrantsForStep, } serversDb.getServer = async (id) => (id === 'main' ? ROW : null) backpacksDb.reportFor = async () => ({ serverId: 'main', backpacks: loaded ? JSON.stringify({ loaded: true, version: '3.17.8', sizes }) : null }) client.tallySnapshot = async (server, runId) => ok({ kind: 'tally.snapshot', runId, people, maxRecipients: 100 }) linksDb.userIdsForSteamIds = async (ids) => ids.filter((id) => links[id]).map((id) => ({ steamId: id, userId: links[id] })) permDb.listRunGrantsForStep = async () => existing permDb.insertRunGrants = async (rows) => { calls.inserted.push(...rows) return rows.length } permDb.markDirty = async (serverId) => calls.dirty.push(serverId) permDb.deleteRunGrantsForStep = async (runId, stepId) => { calls.deleted.push(`${runId}:${stepId}`) return ['main'] } t.after(() => Object.assign(serversDb, { getServer: saved.getServer })) t.after(() => Object.assign(backpacksDb, { reportFor: saved.reportFor })) t.after(() => Object.assign(client, { tallySnapshot: saved.tallySnapshot })) t.after(() => Object.assign(linksDb, { userIdsForSteamIds: saved.userIdsForSteamIds })) t.after(() => Object.assign(permDb, { listRunGrantsForStep: saved.listRunGrantsForStep, insertRunGrants: saved.insertRunGrants, markDirty: saved.markDirty, deleteRunGrantsForStep: saved.deleteRunGrantsForStep, }), ) return calls } const PARAMS = { server: 'main', size: 48, recipients: 'everyone' } const person = (steamId, score) => ({ steamId, name: `p${steamId.slice(-1)}`, seconds: 60, kills: 0, score, joinedAt: 1 }) test('a bigger backpack is a ledgered change, priced per recipient on its own budget', () => { assert.strictEqual(lend.risk, 'change') assert.strictEqual(lend.reversible, 'ledger') assert.deepStrictEqual(lend.cost({ recipients: 'top', count: 3 }), { 'rust.backpacks': 3 }) assert.deepStrictEqual(lend.cost({ recipients: 'linked' }), { 'rust.backpacks': rewards.MAX_RECIPIENTS }) }) test('a server without Backpacks is refused in the dry run, with the reason (D199)', async (t) => { stub(t, { loaded: false }) const result = await lend.perform({ runId: 7, stepId: 4, idempotencyKey: 'k-1', params: PARAMS, verify: true }) assert.strictEqual(result.ok, false) assert.strictEqual(result.retry, false) assert.match(result.error, /Backpacks is not installed on this server \(Main\)/) }) test('a size the server does not offer, or its smallest, is refused by name', async (t) => { stub(t, { sizes: [6, 12, 48] }) for (const size of [6, 96, 0, 'big']) { const result = await lend.perform({ runId: 7, stepId: 4, idempotencyKey: 'k', params: { ...PARAMS, size }, verify: true }) assert.strictEqual(result.ok, false, String(size)) assert.strictEqual(result.retry, false) } const named = await lend.perform({ runId: 7, stepId: 4, idempotencyKey: 'k', params: { ...PARAMS, size: 96 }, verify: true }) assert.match(named.error, /offers 12, 48 slots/) const ok48 = await lend.perform({ runId: 7, stepId: 4, idempotencyKey: 'k', params: PARAMS, verify: true }) assert.deepStrictEqual(ok48, { ok: true }) }) test('the lease is one row per website user: the size’s permission, no kit, no credit (D337)', async (t) => { const calls = stub(t, { // A and B are one person (user 1); C has linked nothing. people: [person(A, 9), person(B, 5), person(C, 3)], links: { [A]: 1, [B]: 1 }, }) const result = await lend.perform({ runId: 7, stepId: 4, idempotencyKey: 'k-1', params: PARAMS, verify: false }) assert.strictEqual(result.ok, true) assert.strictEqual(calls.inserted.length, 1) const [row] = calls.inserted assert.strictEqual(row.permission, 'backpacks.size.48') assert.strictEqual(row.kit, '') assert.strictEqual(row.credit, false) assert.strictEqual(row.steamId, A) assert.strictEqual(row.userId, 1) assert.deepStrictEqual(calls.dirty, ['main']) assert.deepStrictEqual(result.resources, [{ kind: 'backpack-size', ref: 'main:7:4', payload: { serverId: 'main', permission: 'backpacks.size.48' } }]) assert.strictEqual(result.detail.lent, 1) assert.deepStrictEqual(result.detail.missed, ['p3']) }) test('an empty message is the take-back warning, with the size in it (D338)', async (t) => { const calls = stub(t, { people: [person(A, 1)], links: { [A]: 1 } }) await lend.perform({ runId: 7, stepId: 4, idempotencyKey: 'k-1', params: { ...PARAMS, message: ' ' }, verify: false }) assert.match(calls.inserted[0].notice, /^Your backpack holds 48 until the event ends\./) assert.strictEqual(calls.inserted[0].noticeDelivery, 'chat') assert.strictEqual(rewards.backpackNotice({}, 48).text, rewards.BACKPACK_NOTICE) assert.strictEqual(rewards.backpackNotice({ message: 'Big bag: {size}', delivery: 'popup' }, 96).delivery, 'popup') assert.strictEqual(rewards.backpackNotice({ message: 'x', delivery: 'carrier pigeon' }, 96).ok, false) assert.strictEqual(rewards.backpackNotice({ message: `${'x'.repeat(254)}{size}` }, 144).ok, false) }) test('a repeated key finds its rows and lends nothing new', async (t) => { const calls = stub(t, { existing: [{ userId: 1 }, { userId: 2 }], people: [person(A, 1)], links: { [A]: 1 } }) const result = await lend.perform({ runId: 7, stepId: 4, idempotencyKey: 'k-1', params: PARAMS, verify: false }) assert.strictEqual(result.detail.repeat, true) assert.strictEqual(result.detail.lent, 2) assert.strictEqual(calls.inserted.length, 0) }) test('the revert deletes the step’s rows, and the next sync takes the size back', async (t) => { const calls = stub(t) const result = await lend.revert({ runId: 7, resources: [{ kind: 'backpack-size', ref: 'main:7:4' }], idempotencyKey: 'k-1' }) assert.deepStrictEqual(result, { ok: true }) assert.deepStrictEqual(calls.deleted, ['7:4']) assert.deepStrictEqual(calls.dirty, ['main']) }) test('the size list offers each server’s sizes above its smallest, grouped by server', async (t) => { const saved = { listForPolling: servers.listForPolling, reports: backpacksDb.reports } t.after(() => Object.assign(servers, { listForPolling: saved.listForPolling }) && Object.assign(backpacksDb, { reports: saved.reports })) servers.listForPolling = async () => [{ id: 'main', name: 'Main' }, { id: 'pve', name: 'PvE' }] backpacksDb.reports = async () => [ { serverId: 'main', backpacks: JSON.stringify({ loaded: true, sizes: [6, 48] }) }, { serverId: 'pve', backpacks: null }, ] const source = rewards.OPTION_SOURCES.find((s) => s.id === 'rust.options.backpacksizes') assert.deepStrictEqual(await source.resolve(), [{ value: 48, label: '48 slots', group: 'Main' }]) }) // ── Reading one ──────────────────────────────────────────────────────────── function stubRead(t, answer) { const sent = [] const saved = { getForCalling: servers.getForCalling, backpacksRead: client.backpacksRead } t.after(() => Object.assign(servers, { getForCalling: saved.getForCalling }) && Object.assign(client, { backpacksRead: saved.backpacksRead })) servers.getForCalling = async (id) => (id === 'main' ? { id: 'main', name: 'Main', baseUrl: 'http://main:1', token: 't' } : null) client.backpacksRead = async (server, steamId) => { sent.push(steamId) return typeof answer === 'function' ? answer(steamId) : answer } return sent } test('a backpack is read live and reduced to what the grid draws', async (t) => { const sent = stubRead( t, ok({ kind: 'backpacks.contents', steamId: A, exists: true, capacity: 12, pageSize: 48, items: [ { slot: 0, shortname: 'wood', display: 'Wood', amount: 1000 }, { slot: 2, shortname: 'rifle.ak', display: 'Assault Rifle', amount: 1, contents: [{ slot: 0, shortname: 'weapon.mod.lasersight', display: 'Weapon Lasersight', amount: 1, contents: [{ slot: 0 }] }] }, { slot: 3, shortname: 'hoodie', display: 'Hoodie', amount: 1, skin: 10129 }, { slot: 5, shortname: 'blueprintbase', display: 'Assault Rifle', blueprint: 'rifle.ak', amount: 1 }, { slot: 14, shortname: 'note', display: 'Note', amount: 1, name: 'spike-a', overflow: true }, { slot: 15, shortname: null, itemId: 999, amount: 1, overflow: true }, ], }), ) const result = await backpacks.read({ serverId: 'main', steamId: A }) assert.deepStrictEqual(sent, [A]) assert.strictEqual(result.ok, true) const bp = result.backpack assert.strictEqual(bp.server, 'Main') assert.strictEqual(bp.capacity, 12) assert.strictEqual(bp.pageSize, 48) assert.strictEqual(bp.items.length, 6) assert.strictEqual(bp.items[1].contents.length, 1) assert.strictEqual(bp.items[1].contents[0].contents, undefined, 'one level of contents, no deeper') assert.strictEqual(bp.items[2].skin, '10129', 'a skin id is text') assert.strictEqual(bp.items[3].blueprint, 'rifle.ak') assert.strictEqual(bp.items[4].name, 'spike-a') assert.strictEqual(bp.items[4].overflow, true) assert.strictEqual(bp.items[5].itemId, 999) }) test('a player who never had a backpack is exists: false, with no capacity', async (t) => { stubRead(t, ok({ kind: 'backpacks.contents', steamId: A, exists: false })) const result = await backpacks.read({ serverId: 'main', steamId: A }) assert.deepStrictEqual(result.backpack, { serverId: 'main', server: 'Main', steamId: A, exists: false }) }) test('a read says why it has nothing: no server, no Backpacks, or no answer', async (t) => { stubRead(t, ok({ kind: 'backpacks.error', reason: 'backpacks-missing', message: 'Backpacks is not installed on this server' })) const missing = await backpacks.read({ serverId: 'main', steamId: A }) assert.strictEqual(missing.status, 409) assert.strictEqual(missing.message, 'Backpacks is not installed on this server (Main)') assert.strictEqual((await backpacks.read({ serverId: 'nope', steamId: A })).status, 404) client.backpacksRead = async () => ({ ok: false, status: 'http-503' }) const down = await backpacks.read({ serverId: 'main', steamId: A }) assert.strictEqual(down.status, 503) assert.match(down.message, /Main has no game connected/) }) // ── The routes ───────────────────────────────────────────────────────────── function res() { return { statusCode: 200, body: null, status(code) { this.statusCode = code return this }, json(body) { this.body = body return this }, } } test('a backpack is read only for an account its user has linked', async (t) => { const links = require('../model/links/links.model') const saved = { listForUser: links.listForUser } t.after(() => Object.assign(links, saved)) links.listForUser = async (userId) => (userId === 1 ? [{ steamId: A, name: 'pa' }] : []) const sent = stubRead(t, ok({ kind: 'backpacks.contents', steamId: A, exists: false })) const player = require('../router/player/rust.controller') const staff = require('../router/admin/usersRust.controller') const own = res() await player.ownBackpack({ user: { id: 1 }, query: { serverId: 'main', steamId: A } }, own) assert.strictEqual(own.statusCode, 200) assert.strictEqual(own.body.exists, false) const stranger = res() await player.ownBackpack({ user: { id: 1 }, query: { serverId: 'main', steamId: B } }, stranger) assert.strictEqual(stranger.statusCode, 404) const wrongUser = res() await staff.readBackpack({ params: { id: '2' }, query: { serverId: 'main', steamId: A } }, wrongUser) assert.strictEqual(wrongUser.statusCode, 404) const viaStaff = res() await staff.readBackpack({ params: { id: '1' }, query: { serverId: 'main', steamId: A } }, viaStaff) assert.strictEqual(viaStaff.statusCode, 200) assert.deepStrictEqual(sent, [A, A], 'a stranger’s id never reaches the game') })