25 of 82 test files left with the module. Three that core keeps needed splitting rather than moving, and the split is the boundary in each case. announceJobs.test.js keeps the announce PIPELINE -- the shared backoff schedule, the parent-status rollup, core's Discord leg -- and loses the town-crier text building and classification, which are a module's leg. pushDispatch.test.js keeps the SSRF guard and publish() delivering a content-free tickle, and loses mapShardEvent and the shard fan-out, which are a module's catalog. playerRouteAccess.test.js is the one worth explaining. It guards a real past bug -- an admin 403'd off their own characters -- and it did so through /player/shard/accounts, which is now module-owned. The guarantee it protects is CORE's, though: /player/* is role-agnostic self-service, staff are a superset of players. So it stays here and asserts that through /player/appeals, a core route with the same gate. Moving it would have left core with no test of its own tier rule, which is precisely what regressed once before. The remaining updates are core's own tests catching up: ctx has four more members, registerCore now registers only what core owns (one stream, one leg, no filled slot), and the extension-slot test asks for the DECLARED slot's router rather than the filled one, since core declares it and a module fills it. The gated-surface floor drops from >100 to >50 -- it is there so a filter matching nothing fails loudly, not to track core's exact route count. 616 core tests and 160 client tests pass; the module's own suite is 351. Co-Authored-By: Claude <noreply@anthropic.com>
85 lines
3.3 KiB
JavaScript
85 lines
3.3 KiB
JavaScript
// Point the DB at a closed port BEFORE requiring anything that builds the pool,
|
|
// so any stray query fails fast instead of holding the process open. The session
|
|
// service, users model, and appeals model are all stubbed, 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')
|
|
|
|
const { startApp } = require('./_helper')
|
|
const playerRouter = require('../src/router/v1/player')
|
|
const sessionService = require('../src/auth/session.service')
|
|
const users = require('../src/model/users/users.model')
|
|
const appeals = require('../src/model/appeals/appeals.model')
|
|
const db = require('../src/utils/db')
|
|
|
|
after(() => db.close())
|
|
|
|
const originals = {
|
|
validateSession: sessionService.validateSession,
|
|
isSessionRevoked: sessionService.isSessionRevoked,
|
|
sessionMeta: sessionService.sessionMeta,
|
|
getById: users.getById,
|
|
listMine: appeals.listMine,
|
|
}
|
|
afterEach(() => {
|
|
sessionService.validateSession = originals.validateSession
|
|
sessionService.isSessionRevoked = originals.isSessionRevoked
|
|
sessionService.sessionMeta = originals.sessionMeta
|
|
users.getById = originals.getById
|
|
appeals.listMine = originals.listMine
|
|
})
|
|
|
|
// Sign every request in as the given DB user (role decides the gate outcome).
|
|
function signInAs(user) {
|
|
sessionService.validateSession = () => ({ userId: user.id, sessionId: 's1', createdAt: Date.now(), authMethod: 'jwt' })
|
|
sessionService.isSessionRevoked = async () => false
|
|
sessionService.sessionMeta = () => ({})
|
|
users.getById = async () => user
|
|
}
|
|
|
|
// The player self-service group is intentionally role-agnostic (staff are a
|
|
// superset of players): any authenticated, active account reaches the self-scoped
|
|
// handlers. Regression guard for the fix that dropped requireRole('player') so a
|
|
// staff/admin account is no longer 403'd out of its own characters.
|
|
for (const role of ['player', 'admin', 'editor', 'moderator']) {
|
|
test(`GET /player/appeals is reachable by an authenticated ${role}`, async () => {
|
|
signInAs({ id: 7, username: 'u', role, status: 'active' })
|
|
appeals.listMine = async (id) => {
|
|
assert.equal(id, 7) // self-scoped to the caller regardless of role
|
|
return [{ id: 1 }]
|
|
}
|
|
const app = await startApp((a) => a.use('/api/v1/player', playerRouter))
|
|
try {
|
|
const res = await fetch(app.url + '/api/v1/player/appeals')
|
|
assert.equal(res.status, 200, `${role} should reach the handler, got ${res.status}`)
|
|
assert.deepEqual(await res.json(), [{ id: 1 }])
|
|
} finally {
|
|
await app.close()
|
|
}
|
|
})
|
|
}
|
|
|
|
test('GET /player/appeals still rejects an unauthenticated caller with 401', async () => {
|
|
sessionService.validateSession = () => null
|
|
const app = await startApp((a) => a.use('/api/v1/player', playerRouter))
|
|
try {
|
|
const res = await fetch(app.url + '/api/v1/player/appeals')
|
|
assert.equal(res.status, 401)
|
|
} finally {
|
|
await app.close()
|
|
}
|
|
})
|
|
|
|
test('a disabled account is still rejected with 403 (status gate, not role)', async () => {
|
|
signInAs({ id: 8, username: 'banned', role: 'player', status: 'disabled' })
|
|
const app = await startApp((a) => a.use('/api/v1/player', playerRouter))
|
|
try {
|
|
const res = await fetch(app.url + '/api/v1/player/appeals')
|
|
assert.equal(res.status, 403)
|
|
} finally {
|
|
await app.close()
|
|
}
|
|
})
|