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:
154
server/test/adminUserShard.test.js
Normal file
154
server/test/adminUserShard.test.js
Normal file
@@ -0,0 +1,154 @@
|
||||
// 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 anything that builds the pool,
|
||||
// so any stray query fails fast instead of hanging the runner. These tests stub
|
||||
// every model method the controller touches, so the DB is never actually hit.
|
||||
|
||||
const { test, after, afterEach } = require('node:test')
|
||||
const assert = require('node:assert/strict')
|
||||
|
||||
const ctrl = require('../router/admin/usersShard.controller')
|
||||
const { ctx } = require('./_setup')
|
||||
const users = ctx.users
|
||||
const shardLinks = require('../model/shardLinks/shardLinks.model')
|
||||
const shardState = require('../model/shardState/shardState.model')
|
||||
const shardEvents = require('../model/shardEvents/shardEvents.model')
|
||||
const { salesForAccounts } = require('../utils/shardSales')
|
||||
|
||||
|
||||
function mockRes() {
|
||||
return {
|
||||
statusCode: 200,
|
||||
body: null,
|
||||
status(c) {
|
||||
this.statusCode = c
|
||||
return this
|
||||
},
|
||||
json(b) {
|
||||
this.body = b
|
||||
return this
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// Save/restore the originals so each test's monkeypatches don't leak.
|
||||
const originals = {
|
||||
getById: users.getById,
|
||||
listForUser: shardLinks.listForUser,
|
||||
listHousesForAccounts: shardState.listHousesForAccounts,
|
||||
listOnlineForAccounts: shardState.listOnlineForAccounts,
|
||||
eventsList: shardEvents.list,
|
||||
}
|
||||
afterEach(() => {
|
||||
users.getById = originals.getById
|
||||
shardLinks.listForUser = originals.listForUser
|
||||
shardState.listHousesForAccounts = originals.listHousesForAccounts
|
||||
shardState.listOnlineForAccounts = originals.listOnlineForAccounts
|
||||
shardEvents.list = originals.eventsList
|
||||
})
|
||||
|
||||
// ── salesForAccounts util ──────────────────────────────────────────────────
|
||||
test('salesForAccounts returns [] for an empty account set without hitting the log', async () => {
|
||||
let called = false
|
||||
shardEvents.list = async () => {
|
||||
called = true
|
||||
return []
|
||||
}
|
||||
assert.deepEqual(await salesForAccounts([]), [])
|
||||
assert.equal(called, false)
|
||||
})
|
||||
|
||||
test('salesForAccounts keeps only sales owned by the given accounts, newest 50', async () => {
|
||||
const events = []
|
||||
// 60 sales owned by "mine", plus some owned by "other".
|
||||
for (let i = 0; i < 60; i++) {
|
||||
events.push({ t: i, payload: { ownerAcct: 'mine', itemType: 'sword', amount: 1, price: 10, commission: 1 } })
|
||||
}
|
||||
events.push({ t: 999, payload: { ownerAcct: 'other', itemType: 'shield', amount: 1, price: 5 } })
|
||||
shardEvents.list = async () => events
|
||||
|
||||
const rows = await salesForAccounts(['mine'])
|
||||
assert.equal(rows.length, 50) // capped
|
||||
assert.ok(rows.every((r) => r.ownerAcct === 'mine')) // never leaks "other"
|
||||
assert.deepEqual(Object.keys(rows[0]).sort(), ['amount', 'commission', 'itemType', 'ownerAcct', 'price', 't'])
|
||||
})
|
||||
|
||||
// ── Controller: unknown user → 404 ─────────────────────────────────────────
|
||||
for (const handler of ['listAccounts', 'getSales', 'getHouses', 'getOnline']) {
|
||||
test(`${handler} returns 404 when the user does not exist`, async () => {
|
||||
users.getById = async () => null
|
||||
const res = mockRes()
|
||||
await ctrl[handler]({ params: { id: '404' } }, res)
|
||||
assert.equal(res.statusCode, 404)
|
||||
})
|
||||
}
|
||||
|
||||
// ── Controller: scoping to the user's accounts ─────────────────────────────
|
||||
test('listAccounts returns the user’s linked accounts', async () => {
|
||||
users.getById = async () => ({ id: 7, username: 'bob', role: 'player' })
|
||||
shardLinks.listForUser = async (id) => {
|
||||
assert.equal(id, 7)
|
||||
return [{ account: 'acctA' }, { account: 'acctB' }]
|
||||
}
|
||||
const res = mockRes()
|
||||
await ctrl.listAccounts({ params: { id: '7' } }, res)
|
||||
assert.equal(res.statusCode, 200)
|
||||
assert.deepEqual(res.body, [{ account: 'acctA' }, { account: 'acctB' }])
|
||||
})
|
||||
|
||||
test('getHouses passes exactly the user’s accounts to the model', async () => {
|
||||
users.getById = async () => ({ id: 7 })
|
||||
shardLinks.listForUser = async () => [{ account: 'acctA' }, { account: 'acctB' }]
|
||||
let received = null
|
||||
shardState.listHousesForAccounts = async (accounts) => {
|
||||
received = accounts
|
||||
return [{ serial: '0x1', isIdoc: true }]
|
||||
}
|
||||
const res = mockRes()
|
||||
await ctrl.getHouses({ params: { id: '7' } }, res)
|
||||
assert.deepEqual(received, ['acctA', 'acctB'])
|
||||
assert.deepEqual(res.body, [{ serial: '0x1', isIdoc: true }])
|
||||
})
|
||||
|
||||
test('getOnline passes exactly the user’s accounts to the model', async () => {
|
||||
users.getById = async () => ({ id: 7 })
|
||||
shardLinks.listForUser = async () => [{ account: 'acctA' }]
|
||||
let received = null
|
||||
shardState.listOnlineForAccounts = async (accounts) => {
|
||||
received = accounts
|
||||
return [{ serial: '0x2', name: 'Zoe' }]
|
||||
}
|
||||
const res = mockRes()
|
||||
await ctrl.getOnline({ params: { id: '7' } }, res)
|
||||
assert.deepEqual(received, ['acctA'])
|
||||
assert.deepEqual(res.body, [{ serial: '0x2', name: 'Zoe' }])
|
||||
})
|
||||
|
||||
test('a user with no linked accounts yields empty sales/houses/online', async () => {
|
||||
users.getById = async () => ({ id: 7 })
|
||||
shardLinks.listForUser = async () => []
|
||||
shardState.listHousesForAccounts = async (a) => (a.length ? [{}] : [])
|
||||
shardState.listOnlineForAccounts = async (a) => (a.length ? [{}] : [])
|
||||
shardEvents.list = async () => [{ payload: { ownerAcct: 'someoneElse' } }]
|
||||
|
||||
const sales = mockRes()
|
||||
const houses = mockRes()
|
||||
const online = mockRes()
|
||||
await ctrl.getSales({ params: { id: '7' } }, sales)
|
||||
await ctrl.getHouses({ params: { id: '7' } }, houses)
|
||||
await ctrl.getOnline({ params: { id: '7' } }, online)
|
||||
|
||||
assert.deepEqual(sales.body, [])
|
||||
assert.deepEqual(houses.body, [])
|
||||
assert.deepEqual(online.body, [])
|
||||
})
|
||||
|
||||
// getUser is NOT here any more: reading a user is core semantics that had ended
|
||||
// up in this controller by proximity, and PR 4 moved it back to
|
||||
// admin.controller.js behind the extension slot (MODULE_SYSTEM.md §1.9). It is
|
||||
// covered by test/adminUsers.test.js.
|
||||
Reference in New Issue
Block a user