Files
Module-uo/server/test/uoLinkClient.test.js
wtclaude 6b99d7e220
All checks were successful
PR Checks / client-build (pull_request) Successful in 17s
PR Checks / server-tests (pull_request) Successful in 8m47s
test(server): port core's UO suite onto the ctx harness
22 test files moved from core, plus the two that were split out of files core
keeps. 351 tests pass.

One change runs through every moved test, and it is the boundary rather than a
chore: core internals can no longer be stubbed by requiring them, because there
are none to require. `../utils/db` and `../model/settings` do not exist here.
What a test controls instead is the ctx core would have handed over, installed
once by test/_setup.js -- which is a better seam anyway, since it is exactly the
surface the contract promises and nothing wider.

The ctx _setup installs is deliberately unfrozen. Core freezes what it hands a
module and entry.test.js still asserts against a frozen one; but a test that
needs settings.get to return a path has to be able to say so.

Two tests changed SHAPE, and that is the boundary too. fromShardEvent used to
assert through publish() into pushDevices and a captured fetch -- which
endpoints were hit, how many requests went out. None of that is this module's
any more: publish is ctx.push.publish, and the device registry and the relay are
behind it. Reaching for them from here would be reaching past ctx. What remains
is what the module owns and is the part worth guarding: a game account resolves
to a website user, a personal target that resolves to nobody is dropped rather
than published, and a sensitive kind never reaches publish at all.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-08-11 12:07:15 -05:00

75 lines
3.2 KiB
JavaScript

// Ported from core in Phase 3 (MODULE_SYSTEM.md §2.7.1). One change runs through
// every moved test: core internals can no longer be stubbed by requiring them,
// because there are none to require — `../utils/db` and `../model/settings` do
// not exist here. What a test controls instead is the `ctx` core would have
// handed over, installed once by `test/_setup.js`, which is the seam the
// contract actually promises.
// Point the DB at a closed port BEFORE requiring the modules (they build the pool).
// The uoLinkConfig model is monkeypatched so no query runs.
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('../utils/uoLinkClient')
const uoLinkConfig = require('../model/uoLinkConfig/uoLinkConfig.model')
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)
})