// ── Identity: the fleet loop and the refusal ────────────────────────────── // // Two things in this file are worth more than the rest, and both are about // telling answers apart that a naive implementation collapses: // // • **A code is minted by ONE server** and the player types six characters into // a browser. Every server is asked in turn (D24), and "every reachable server // said no" is NOT the same answer as "a server could not be reached" — the // second is the case where the player's code is perfectly good and the advice // "run /link again" is useless, because it sends them back to the server that // is down. // // • **A Steam id another account holds is refused, never moved** (D23). Once // phase 7 grants permissions against a link and phase 13 hangs entitlements // off it, a silent move is an account takeover performed by typing six // characters. const test = require('node:test') const assert = require('node:assert') const { fakeCtx } = require('./_fakes') /** * Installs a ctx whose `db.query` answers from a small script. * * `rows` is consulted by the first word of the statement, which is as much SQL as * these tests should know: the point of each one is the decision the model makes, * not the shape of a SELECT it delegates. */ function withCore({ select = [], onInsert = null } = {}) { const queries = [] const ctx = fakeCtx({ db: { query: (sql, params) => { queries.push({ sql, params }) const verb = sql.trim().split(/\s+/)[0].toUpperCase() if (verb === 'SELECT') { const next = Array.isArray(select) ? select.shift() : select return Promise.resolve(next || []) } if (verb === 'INSERT' && onInsert) return onInsert(params) return Promise.resolve({ affectedRows: 1 }) }, pool: {}, }, }) require('../core')._reset() require('../core').init(ctx) return { ctx, queries } } /** A fleet of `n` servers, and a sidecar that answers from a script. */ function fleetOf(replies) { const servers = require('../model/servers/servers.model') const sidecar = require('../sidecarClient') const asked = [] const ids = Object.keys(replies) servers.listForPolling = async () => ids.map((id) => ({ id, baseUrl: `http://${id}`, token: 't' })) sidecar.confirmLink = async (server, code) => { asked.push({ server: server.id, code }) return replies[server.id] } return asked } /** The two replies a reachable sidecar can carry, and the one it cannot. */ const linkOk = (steamId, name) => ({ ok: true, status: 'ok', data: { kind: 'link.ok', steamId, name } }) const linkRefused = { ok: true, status: 'ok', data: { kind: 'link.error', reason: 'unknown' } } const unreachable = { ok: false, status: 'transport-error', data: null } test('every server is asked until one recognises the code, and the one that answered is recorded', async () => { const { queries } = withCore({ select: [[], [{ steamId: '7656', userId: 4, name: 'Wanderer', serverId: 'b' }]] }) const links = require('../model/links/links.model') const asked = fleetOf({ a: linkRefused, b: linkOk('7656', 'Wanderer') }) const result = await links.redeem({ code: 'K7M2PQ', userId: 4 }) assert.equal(result.ok, true) assert.equal(result.link.steamId, '7656') // Both servers were asked, in order, with the same code — and the loop stopped // at the one that said yes. assert.deepEqual(asked, [{ server: 'a', code: 'K7M2PQ' }, { server: 'b', code: 'K7M2PQ' }]) // The server that minted it is stored. It is not part of the identity — a link // is fleet-wide — but it is where a support conversation starts. const insert = queries.find((q) => q.sql.trim().toUpperCase().startsWith('INSERT')) assert.deepEqual(insert.params, ['7656', 4, 'Wanderer', 'b']) }) test('a server after the one that answered is never asked', async () => { withCore({ select: [[], [{ steamId: '7656', userId: 4 }]] }) const links = require('../model/links/links.model') const asked = fleetOf({ a: linkOk('7656', 'Wanderer'), b: linkRefused, c: linkRefused }) await links.redeem({ code: 'K7M2PQ', userId: 4 }) // A code is spent on the plugin's FIRST lookup, so carrying on after a yes // would be asking four other game hosts to look up a secret that has already // been redeemed. assert.deepEqual(asked.map((a) => a.server), ['a']) }) test('a Steam id another account holds is refused, not moved — and the loop stops', async () => { // The whole of D23 in one assertion. The holder is named because the player is // signed in and the advice ("sign in as that account, or run /unlink") is // unusable without it. withCore({ select: [[{ steamId: '7656', userId: 9, username: 'someone-else' }]] }) const links = require('../model/links/links.model') const asked = fleetOf({ a: linkOk('7656', 'Wanderer'), b: linkRefused }) const result = await links.redeem({ code: 'K7M2PQ', userId: 4 }) assert.equal(result.ok, false) assert.equal(result.reason, 'taken') assert.equal(result.username, 'someone-else') // Asking the rest of the fleet would answer the same question more slowly: the // verdict is about the Steam id, not about this server. assert.deepEqual(asked.map((a) => a.server), ['a']) }) test('a code already redeemed by the SAME user is a success, not an error', async () => { withCore({ select: [[{ steamId: '7656', userId: 4, name: 'Wanderer', serverId: 'a' }]] }) const links = require('../model/links/links.model') fleetOf({ a: linkOk('7656', 'Wanderer') }) const result = await links.redeem({ code: 'K7M2PQ', userId: 4 }) // A player who pressed the button twice, or whose confirmation was applied on a // request that then timed out. Reporting that as a failure would send them to // run `/link` again for a link they already have. assert.equal(result.ok, true) assert.equal(result.already, true) }) test('"every reachable server refused" is not the same answer as "a server was unreachable"', async () => { withCore() const links = require('../model/links/links.model') fleetOf({ a: linkRefused, b: unreachable }) const result = await links.redeem({ code: 'K7M2PQ', userId: 4 }) // The failure this prevents: a player linked on the server that is down, is // told their code is wrong, runs `/link` again on that same server, and is told // the same thing for as long as it stays down. assert.equal(result.reason, 'unsure') }) test('a fleet nobody can reach is offline, and a fleet that all refused is a bad code', async () => { withCore() let links = require('../model/links/links.model') fleetOf({ a: unreachable, b: unreachable }) assert.equal((await links.redeem({ code: 'K7M2PQ', userId: 4 })).reason, 'offline') withCore() links = require('../model/links/links.model') fleetOf({ a: linkRefused, b: linkRefused }) assert.equal((await links.redeem({ code: 'K7M2PQ', userId: 4 })).reason, 'rejected') }) test('a site with no servers configured says so rather than that the code is wrong', async () => { withCore() const links = require('../model/links/links.model') fleetOf({}) assert.equal((await links.redeem({ code: 'K7M2PQ', userId: 4 })).reason, 'no-servers') }) test('two confirmations of one Steam id race into the primary key, not into a 500', async () => { // The window the PRIMARY KEY exists for: both requests read "not linked", both // write. The second insert is refused by the key, and the refusal has to become // the same sentence the check above produces — otherwise one of two players // pressing a button at the same moment gets an internal error. const dup = Object.assign(new Error('duplicate'), { code: 'ER_DUP_ENTRY' }) withCore({ select: [[], [{ steamId: '7656', userId: 9, username: 'someone-else' }]], onInsert: () => Promise.reject(dup), }) const links = require('../model/links/links.model') fleetOf({ a: linkOk('7656', 'Wanderer') }) const result = await links.redeem({ code: 'K7M2PQ', userId: 4 }) assert.equal(result.ok, false) assert.equal(result.reason, 'taken') assert.equal(result.username, 'someone-else') }) test('the same race, won by the caller, is a success', async () => { const dup = Object.assign(new Error('duplicate'), { errno: 1062 }) withCore({ select: [[], [{ steamId: '7656', userId: 4, name: 'Wanderer', serverId: 'a' }]], onInsert: () => Promise.reject(dup), }) const links = require('../model/links/links.model') fleetOf({ a: linkOk('7656', 'Wanderer') }) const result = await links.redeem({ code: 'K7M2PQ', userId: 4 }) assert.equal(result.ok, true) assert.equal(result.already, true) }) test('a link is never shaped with anything a code could be recovered from', async () => { withCore() const links = require('../model/links/links.model') const shaped = links.shape({ steamId: '7656', userId: 4, username: 'someone', name: 'Wanderer', serverId: 'a', linkedAt: '2026-09-21T00:00:00Z', }) // `userId` and `username` are deliberately absent: the caller is the user, and // a list that carried somebody's website username would be a different fact // from "you hold this Steam id". assert.deepEqual(Object.keys(shaped).sort(), ['linkedAt', 'name', 'serverId', 'steamId']) }) test('an unlink is scoped by user in the statement, not checked before it', async () => { const { queries } = withCore() const links = require('../model/links/links.model') await links.unlinkOwned('7656', 4) const del = queries.find((q) => q.sql.trim().toUpperCase().startsWith('DELETE')) // Read-then-write would leave a gap between the ownership test and the // deletion; one statement closes it, and the row count is what tells "removed" // from "was not yours". assert.ok(del.sql.includes('user_id = ?')) assert.deepEqual(del.params, ['7656', 4]) }) test('the in-game unlink is scoped by Steam id alone, because that is the authority', async () => { const { queries } = withCore() const links = require('../model/links/links.model') await links.unlinkFromGame('7656') const del = queries.find((q) => q.sql.trim().toUpperCase().startsWith('DELETE')) // Whoever is connected to the game as that Steam account is who it is — a // stronger proof of ownership than the site can obtain any other way. Scoping // this by website user would make `/unlink` fail for the one player who needs // it: the one who linked the wrong account. assert.ok(!del.sql.includes('user_id')) assert.deepEqual(del.params, ['7656']) }) test('a player sees the name the GAME last saw, not the one they linked under', async () => { withCore({ select: [[{ steamId: '7656', userId: 4, name: 'Wanderer-old', playerName: 'Wanderer', serverId: 'a', linkedAt: 'x' }]] }) const links = require('../model/links/links.model') const [link] = await links.listForUser(4) // Found in a browser: staff saw `Wanderer` on the admin panel and the player // saw `Wanderer-old` on their own page — the same person, labelled two ways on // one site, because a Rust name changes on a whim and only one of the two reads // was joining `rust_players`. assert.equal(link.name, 'Wanderer') // And the fallback still holds for a link whose account has never played. withCore({ select: [[{ steamId: '7656', userId: 4, name: 'Wanderer-old', playerName: null, linkedAt: 'x' }]] }) const again = require('../model/links/links.model') assert.equal((await again.listForUser(4))[0].name, 'Wanderer-old') }) test('a new link and a removed one ask core to reconcile Teams (D57)', async () => { // A clan member's website account comes from this table. Without the request, // somebody who links today is not in their clan's Team until core's next // scheduled sweep. const { ctx } = withCore({ select: [[], [{ steamId: '7656', userId: 4 }]] }) const links = require('../model/links/links.model') fleetOf({ a: linkOk('7656', 'Wanderer') }) await links.redeem({ code: 'K7M2PQ', userId: 4 }) assert.equal(ctx.teams.reconcile.calls.length, 1) await links.unlinkAnyOwner('7656') assert.equal(ctx.teams.reconcile.calls.length, 2) }) test('a link that was already there asks for nothing', async () => { const { ctx } = withCore({ select: [[{ steamId: '7656', userId: 4 }]] }) const links = require('../model/links/links.model') fleetOf({ a: linkOk('7656', 'Wanderer') }) const result = await links.redeem({ code: 'K7M2PQ', userId: 4 }) assert.equal(result.already, true) assert.equal(ctx.teams.reconcile.calls.length, 0) })