// ── The field an error has to be in ─────────────────────────────────────── // // **The walk found this, and no test could have.** Core's request primitive is // the only thing that reads a module's failures: // // const message = (data && data.message) || res.statusText || 'Request failed' // // So a body shaped `{ error: '…' }` is not rendered as a worse message — it is // not rendered at all. The player sees `Service Unavailable`, which is what the // link page showed for every one of the four sentences this phase exists to // write, until a browser said so. // // This module answered `{ error }` from its first phase and got away with it, // because until now every failure landed in `ErrorState` on a page whose whole // content was missing — where a generic sentence is honest. A form is different: // the sentence IS the outcome, and the four are not interchangeable. // // The rule is core's `Error` schema (`{ message }`), which every one of this // module's `#swagger.responses` already pointed at. So this suite is the schema // those annotations claim, asserted against what the handlers actually send. const test = require('node:test') const assert = require('node:assert') const { fakeCtx } = require('./_fakes') function withCore() { require('../core')._reset() require('../core').init(fakeCtx()) } /** A response double that records the status and the body. */ function fakeRes() { const res = { statusCode: 200, body: null, status(code) { res.statusCode = code return res }, json(body) { res.body = body return res }, } return res } /** Every outcome `redeem` can answer, and the status each has to become. */ const OUTCOMES = [ [{ ok: false, reason: 'taken', username: 'someone-else' }, 409, /already linked to someone-else/], [{ ok: false, reason: 'unsure' }, 503, /still good/], [{ ok: false, reason: 'offline' }, 503, /unreachable/], [{ ok: false, reason: 'no-servers' }, 503, /No Rust servers/], [{ ok: false, reason: 'rejected' }, 400, /unknown or has expired/], ] test('every refusal reaches the player as a sentence, in the field core reads', async () => { for (const [outcome, status, matches] of OUTCOMES) { withCore() const links = require('../model/links/links.model') const controller = require('../router/player/rust.controller') links.redeem = async () => outcome const res = fakeRes() await controller.confirmLink({ body: { code: 'K7M2PQ' }, user: { id: 4 } }, res) assert.equal(res.statusCode, status, `${outcome.reason} must be ${status}`) assert.equal(typeof res.body.message, 'string', `${outcome.reason} sent no \`message\``) assert.match(res.body.message, matches) // The half that is easy to leave behind while fixing this: a body carrying // BOTH fields reads correctly in a browser and keeps the wrong shape alive // for the next route that copies it. assert.equal(res.body.error, undefined, `${outcome.reason} still carries an \`error\` field`) } }) test('the five outcomes are five different statuses-and-sentences, not one', async () => { const seen = new Set() for (const [outcome] of OUTCOMES) { withCore() const links = require('../model/links/links.model') const controller = require('../router/player/rust.controller') links.redeem = async () => outcome const res = fakeRes() await controller.confirmLink({ body: { code: 'K7M2PQ' }, user: { id: 4 } }, res) seen.add(res.body.message) } // "That code is wrong" and "we could not reach the server that has it" send a // player to do different things, and one of the two is a dead end when it is // wrong — they run /link again on the server that is down and get the same // answer for as long as it stays down. assert.equal(seen.size, OUTCOMES.length, 'two outcomes tell the player the same thing') }) test('no handler in this module answers in a field core cannot read', async () => { // The other controllers, the same way — driven rather than grepped, because the // shape that matters is what a handler SENDS. Each is given a model that throws, // which is every controller's own 500 path and the one branch they all have. withCore() const cases = [ ['public', '../router/public/rust.controller', 'listServers', { params: {}, query: {} }], ['player', '../router/player/rust.controller', 'listServers', { params: {}, query: {}, user: { id: 4 } }], ['player', '../router/player/rust.controller', 'listLinks', { params: {}, user: { id: 4 } }], ['admin', '../router/admin/rust.controller', 'listServers', { params: {}, query: {} }], ['slot', '../router/admin/usersRust.controller', 'listLinks', { params: { id: '4' } }], ] for (const [tier, modulePath, handler, req] of cases) { withCore() // Core's `query` is the fake's spy; make it throw so every handler takes its // failure branch. require('../core')._reset() require('../core').init(fakeCtx({ db: { query: () => Promise.reject(new Error('the database is not there')), pool: {} }, })) const controller = require(modulePath) const res = fakeRes() await controller[handler](req, res) assert.equal(res.statusCode, 500, `${tier}.${handler} did not fail`) assert.equal(typeof res.body.message, 'string', `${tier}.${handler} sent no \`message\``) assert.equal(res.body.error, undefined, `${tier}.${handler} answers in \`error\``) } })