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>
This commit is contained in:
242
server/test/atlasController.test.js
Normal file
242
server/test/atlasController.test.js
Normal file
@@ -0,0 +1,242 @@
|
||||
// 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 controllers (their models
|
||||
// build the pool). Every model call is monkeypatched, so no query runs;
|
||||
// db.close() at the end releases the pool so the process exits cleanly.
|
||||
|
||||
const { test, after, afterEach } = require('node:test')
|
||||
const assert = require('node:assert/strict')
|
||||
|
||||
// The spawn atlas API, public and admin. What is worth asserting here is not the
|
||||
// SQL (that is the parser suite's job) but the contracts the two surfaces make:
|
||||
//
|
||||
// • the public reads project through the visibility framework — v3.md §3.6.1's
|
||||
// standing rule is that a read path returning shard data and not calling
|
||||
// projectFeature is a bug, and `atlas` declaring no sensitive fields TODAY is
|
||||
// exactly why the call has to be there before one does;
|
||||
// • the public /meta route reports the game world only, never the operator's
|
||||
// filesystem — the ServUO path, the per-file hashes and any pending refresh
|
||||
// stay on the admin route;
|
||||
// • a missing creature is a 404, not an empty 200;
|
||||
// • an unreadable ServUO tree is a 200 carrying `status: 'unavailable'`, NOT a
|
||||
// 500. The refresh contract reports outcomes rather than throwing (so boot is
|
||||
// never blocked by a bad tree), and the admin needs to be told what is wrong
|
||||
// with their path;
|
||||
// • a model failure degrades to a 500 rather than a thrown/uncaught error.
|
||||
const pub = require('../router/public/atlas.controller')
|
||||
const admin = require('../router/admin/shardAtlas.controller')
|
||||
const atlas = require('../model/shardAtlas/shardAtlas.model')
|
||||
const { ctx } = require('./_setup')
|
||||
const activity = ctx.activity
|
||||
const visibility = require('../utils/shardVisibility')
|
||||
|
||||
|
||||
// Stub the visibility MODEL rather than the util's exports: project() calls the
|
||||
// module-internal getConfig, which an exports-level stub would not intercept — it
|
||||
// would hit the closed DB port and cost a ~10s pool timeout per test before
|
||||
// falling back to these same defaults.
|
||||
const visibilityModel = require('../model/shardVisibility/shardVisibility.model')
|
||||
visibilityModel.listAll = async () => [] // no overrides ⇒ compiled defaults
|
||||
visibility.viewerLevel = async (req) => req?.viewerLevel || 'anonymous'
|
||||
|
||||
// The admin controller logs every action; keep it off the DB.
|
||||
activity.log = async () => {}
|
||||
|
||||
function mockRes() {
|
||||
return {
|
||||
statusCode: 200,
|
||||
body: null,
|
||||
status(c) {
|
||||
this.statusCode = c
|
||||
return this
|
||||
},
|
||||
json(b) {
|
||||
this.body = b
|
||||
return this
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
const originals = {
|
||||
searchCreatures: atlas.searchCreatures,
|
||||
getCreature: atlas.getCreature,
|
||||
listRegions: atlas.listRegions,
|
||||
listLandmarks: atlas.listLandmarks,
|
||||
listChampions: atlas.listChampions,
|
||||
publicMeta: atlas.publicMeta,
|
||||
status: atlas.status,
|
||||
refresh: atlas.refresh,
|
||||
approvePending: atlas.approvePending,
|
||||
rejectPending: atlas.rejectPending,
|
||||
setServuoPath: atlas.setServuoPath,
|
||||
}
|
||||
afterEach(() => Object.assign(atlas, originals))
|
||||
|
||||
// ── Public reads ────────────────────────────────────────────────────────
|
||||
test('getCreatures passes the search through and returns the page shape', async () => {
|
||||
let seen = null
|
||||
atlas.searchCreatures = async (opts) => {
|
||||
seen = opts
|
||||
return { total: 1, limit: 50, offset: 0, creatures: [{ slug: 'lizardman', name: 'Lizardman' }] }
|
||||
}
|
||||
const res = mockRes()
|
||||
await pub.getCreatures({ query: { q: ' lizard ', facet: 'Felucca', limit: '10', offset: '20' } }, res)
|
||||
assert.deepEqual(seen, { q: 'lizard', facet: 'Felucca', limit: 10, offset: 20 })
|
||||
assert.equal(res.body.total, 1)
|
||||
assert.equal(res.body.creatures[0].slug, 'lizardman')
|
||||
})
|
||||
|
||||
test('getCreatures falls back to the documented defaults when nothing is passed', async () => {
|
||||
let seen = null
|
||||
atlas.searchCreatures = async (opts) => {
|
||||
seen = opts
|
||||
return { total: 0, limit: 50, offset: 0, creatures: [] }
|
||||
}
|
||||
await pub.getCreatures({ query: {} }, mockRes())
|
||||
assert.deepEqual(seen, { q: '', facet: '', limit: 50, offset: 0 })
|
||||
})
|
||||
|
||||
test('an unknown creature is a 404, not an empty 200', async () => {
|
||||
atlas.getCreature = async () => null
|
||||
const res = mockRes()
|
||||
await pub.getCreature({ params: { slug: 'nosuchthing' }, query: {} }, res)
|
||||
assert.equal(res.statusCode, 404)
|
||||
})
|
||||
|
||||
test('getCreature returns places and spawners, and `points` stays the COUNT', async () => {
|
||||
atlas.getCreature = async () => ({
|
||||
slug: 'lizardman',
|
||||
name: 'Lizardman',
|
||||
total: 214,
|
||||
points: 62,
|
||||
places: [{ facet: 'Trammel', label: 'Shrines', spawners: 7, maxAlive: 21 }],
|
||||
spawners: [{ id: 1, facet: 'Trammel', label: 'Shrines', x: 1, y: 2 }],
|
||||
spawnersTruncated: false,
|
||||
alsoHere: [],
|
||||
})
|
||||
const res = mockRes()
|
||||
await pub.getCreature({ params: { slug: 'lizardman' }, query: {} }, res)
|
||||
// The list route uses `points` as a number; the detail route must not quietly
|
||||
// turn the same key into an array.
|
||||
assert.equal(typeof res.body.points, 'number')
|
||||
assert.ok(Array.isArray(res.body.spawners))
|
||||
assert.equal(res.body.places[0].label, 'Shrines')
|
||||
})
|
||||
|
||||
// ── The projection rule (§3.6.1) ────────────────────────────────────────
|
||||
test('public reads run through projectFeature, so a locked field can never survive', async () => {
|
||||
// `atlas` declares no sensitive fields, so nothing here is stripped by a
|
||||
// FEATURE rule. acct/webId are stripped anyway — they are locked by meaning,
|
||||
// for every feature, and this is what proves the read path projects at all.
|
||||
atlas.searchCreatures = async () => ({
|
||||
total: 1,
|
||||
limit: 50,
|
||||
offset: 0,
|
||||
creatures: [{ slug: 'lizardman', name: 'Lizardman', acct: 'someacct', ownerWebId: 7 }],
|
||||
})
|
||||
const res = mockRes()
|
||||
await pub.getCreatures({ query: {}, viewerLevel: 'anonymous' }, res)
|
||||
const row = res.body.creatures[0]
|
||||
assert.equal(row.name, 'Lizardman')
|
||||
assert.ok(!('acct' in row), 'acct must never reach an anonymous caller')
|
||||
assert.ok(!('ownerWebId' in row), 'a flattened webId spelling is locked too')
|
||||
})
|
||||
|
||||
test('getMeta reports the game world only — never the operator’s filesystem', async () => {
|
||||
// The model is what enforces this; the assertion documents the boundary so a
|
||||
// future "just return status() here" shortcut fails loudly.
|
||||
atlas.publicMeta = async () => ({
|
||||
importedAt: '2026-07-28T00:00:00.000Z',
|
||||
generatedAt: '2026-07-28T00:00:00.000Z',
|
||||
counts: { points: 6455, creatures: 800 },
|
||||
facets: ['Felucca', 'Trammel'],
|
||||
})
|
||||
const res = mockRes()
|
||||
await pub.getMeta({ query: {} }, res)
|
||||
assert.deepEqual(Object.keys(res.body).sort(), ['counts', 'facets', 'generatedAt', 'importedAt'])
|
||||
assert.ok(!('path' in res.body))
|
||||
assert.ok(!('pending' in res.body))
|
||||
})
|
||||
|
||||
test('a model failure degrades to a 500 rather than throwing', async () => {
|
||||
atlas.listChampions = async () => {
|
||||
throw new Error('table is gone')
|
||||
}
|
||||
const res = mockRes()
|
||||
await pub.getChampions({ query: {} }, res)
|
||||
assert.equal(res.statusCode, 500)
|
||||
})
|
||||
|
||||
// ── Admin ───────────────────────────────────────────────────────────────
|
||||
test('an unreadable tree answers 200 with the reason, not a 500', async () => {
|
||||
atlas.refresh = async () => ({ status: 'unavailable', reason: 'no Spawns directory', path: '/bad' })
|
||||
const res = mockRes()
|
||||
await admin.importAtlas({ body: {}, user: { id: 1 } }, res)
|
||||
assert.equal(res.statusCode, 200)
|
||||
assert.equal(res.body.status, 'unavailable')
|
||||
assert.equal(res.body.reason, 'no Spawns directory')
|
||||
})
|
||||
|
||||
test('import passes `force` through and coerces it to a boolean', async () => {
|
||||
let seen = null
|
||||
atlas.refresh = async (opts) => {
|
||||
seen = opts
|
||||
return { status: 'unchanged' }
|
||||
}
|
||||
await admin.importAtlas({ body: { force: true }, user: { id: 1 } }, mockRes())
|
||||
assert.deepEqual(seen, { force: true })
|
||||
})
|
||||
|
||||
test('approve applies a staged refresh (facet loss included)', async () => {
|
||||
let called = false
|
||||
atlas.approvePending = async () => {
|
||||
called = true
|
||||
return { status: 'imported', removedFacets: ['Malas'], counts: { points: 6162 } }
|
||||
}
|
||||
const res = mockRes()
|
||||
await admin.approve({ user: { id: 1 } }, res)
|
||||
assert.ok(called)
|
||||
assert.equal(res.body.status, 'imported')
|
||||
})
|
||||
|
||||
test('rejecting when nothing is staged is a 404', async () => {
|
||||
atlas.rejectPending = async () => ({ status: 'none' })
|
||||
const res = mockRes()
|
||||
await admin.reject({ user: { id: 1 } }, res)
|
||||
assert.equal(res.statusCode, 404)
|
||||
})
|
||||
|
||||
test('setPath trims, persists, and answers with fresh status — it does not import', async () => {
|
||||
let saved = null
|
||||
let imported = false
|
||||
atlas.setServuoPath = async (value) => {
|
||||
saved = value
|
||||
}
|
||||
atlas.refresh = async () => {
|
||||
imported = true
|
||||
return { status: 'imported' }
|
||||
}
|
||||
atlas.status = async () => ({ configured: true, path: '/srv/servuo', treeReadable: true })
|
||||
const res = mockRes()
|
||||
await admin.setPath({ body: { path: ' /srv/servuo ' }, user: { id: 3 } }, res)
|
||||
assert.equal(saved, '/srv/servuo')
|
||||
assert.equal(imported, false, 'changing the path must not reload the atlas as a side effect')
|
||||
assert.equal(res.body.path, '/srv/servuo')
|
||||
})
|
||||
|
||||
test('setPath accepts a blank path (clearing it turns the atlas off)', async () => {
|
||||
let saved = 'unset'
|
||||
atlas.setServuoPath = async (value) => {
|
||||
saved = value
|
||||
}
|
||||
atlas.status = async () => ({ configured: false, path: '' })
|
||||
const res = mockRes()
|
||||
await admin.setPath({ body: {}, user: { id: 3 } }, res)
|
||||
assert.equal(saved, '')
|
||||
assert.equal(res.statusCode, 200)
|
||||
})
|
||||
Reference in New Issue
Block a user