// Point the DB at a closed port BEFORE requiring the modules (they build the pool). // The uoLinkConfig model is monkeypatched so no query runs. process.env.DB_HOST = '127.0.0.1' process.env.DB_PORT = '59999' const { test, after, afterEach } = require('node:test') const assert = require('node:assert/strict') // The uo-link REST client's headline contract (see its module header and // CLAUDE.md): it NEVER throws — every call resolves to { ok, data, status, error } // so a public page or an admin poll degrades to "shard unavailable" instead of // 500ing. The regression these tests lock down: resolveConfig() decrypts the // stored auth token, and secretBox.decrypt THROWS when the ciphertext can't be // authenticated (SECRET_ENC_KEY rotated, or a DB dump restored under a different // key). It used to run OUTSIDE call()'s try, so that throw escaped the client and // 500'd every live-shard route. const uoLinkClient = require('../src/utils/uoLinkClient') const uoLinkConfig = require('../src/model/uoLinkConfig/uoLinkConfig.model') const db = require('../src/utils/db') after(() => db.close()) const origGetWithToken = uoLinkConfig.getWithToken afterEach(() => { uoLinkConfig.getWithToken = origGetWithToken uoLinkClient.invalidateConfig() // drop the 5s config cache between cases }) test('an undecryptable stored token resolves to { ok: false } instead of throwing', async () => { uoLinkConfig.getWithToken = async () => { // Exactly what crypto's Decipheriv.final() raises on a bad key / tampered blob. throw new Error('Unsupported state or unable to authenticate data') } uoLinkClient.invalidateConfig() const result = await uoLinkClient.health() assert.equal(result.ok, false, 'must report failure, not throw') assert.equal(result.status, 0) assert.match(result.error, /unreadable/i, 'distinguishes config failure from a dead sidecar') }) test('every read helper stays on the { ok:false } contract when config is unreadable', async () => { uoLinkConfig.getWithToken = async () => { throw new Error('Unsupported state or unable to authenticate data') } uoLinkClient.invalidateConfig() // The routes that regressed: character sheet, roster and vendor lookups, which // are reachable from both /admin/shard/* and the player-facing /player/shard/*. for (const call of [ () => uoLinkClient.getCharBySerial('0x1'), () => uoLinkClient.getRoster('someacct'), () => uoLinkClient.getVendors('someacct'), ]) { const result = await call() assert.equal(result.ok, false) assert.equal(result.status, 0) } }) test('a missing/blank config still reports "not configured" (unchanged behaviour)', async () => { uoLinkConfig.getWithToken = async () => null uoLinkClient.invalidateConfig() const result = await uoLinkClient.health() assert.equal(result.ok, false) assert.equal(result.status, 0) assert.match(result.error, /not configured/i) })