test: re-point core's suite at what core still owns
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>
This commit is contained in:
@@ -4,62 +4,12 @@ const assert = require('node:assert/strict')
|
||||
const logic = require('../src/model/announceJobs/announceJobs.logic')
|
||||
// Each leg owns its own text-building and result classification since PR 4 — a
|
||||
// leg is a registration now, not a branch in the worker (MODULE_SYSTEM.md §1.8).
|
||||
const townCrier = require('../src/utils/shardAnnounce')
|
||||
const discord = require('../src/utils/discordAnnounce')
|
||||
|
||||
// ── buildTownCrierText ───────────────────────────────────────────────────────
|
||||
test('buildTownCrierText produces title, excerpt, and URL lines', () => {
|
||||
const lines = townCrier.buildTownCrierText(
|
||||
{ id: 7, title: 'Server Update', excerpt: 'Big things afoot.', body: null },
|
||||
{ baseUrl: 'https://uom.example' },
|
||||
)
|
||||
assert.deepEqual(lines, ['Server Update', 'Big things afoot.', 'https://uom.example/site/news'])
|
||||
})
|
||||
|
||||
test('buildTownCrierText falls back to a stripped body when excerpt is empty', () => {
|
||||
const lines = townCrier.buildTownCrierText(
|
||||
{ id: 1, title: 'T', excerpt: '', body: '<p>Hello <b>world</b></p>' },
|
||||
{ baseUrl: 'https://uom.example' },
|
||||
)
|
||||
assert.equal(lines[1], 'Hello world')
|
||||
})
|
||||
|
||||
test('buildTownCrierText clamps each line to the sidecar per-line cap', () => {
|
||||
const longTitle = 'x'.repeat(500)
|
||||
const lines = townCrier.buildTownCrierText(
|
||||
{ id: 1, title: longTitle, excerpt: 'y'.repeat(500), body: null },
|
||||
{ baseUrl: 'https://uom.example' },
|
||||
)
|
||||
for (const line of lines) assert.ok(line.length <= townCrier.MAX_LINE_LEN, `line too long: ${line.length}`)
|
||||
assert.ok(lines[0].endsWith('…'))
|
||||
assert.ok(lines.length <= townCrier.MAX_LINES)
|
||||
})
|
||||
|
||||
test('buildTownCrierText omits the excerpt line when there is no excerpt or body', () => {
|
||||
const lines = townCrier.buildTownCrierText(
|
||||
{ id: 1, title: 'Only a title', excerpt: null, body: null },
|
||||
{ baseUrl: 'https://uom.example' },
|
||||
)
|
||||
assert.deepEqual(lines, ['Only a title', 'https://uom.example/site/news'])
|
||||
})
|
||||
|
||||
// ── classifyTownCrier ────────────────────────────────────────────────────────
|
||||
test('town crier classify: 2xx is done', () => {
|
||||
assert.equal(townCrier.classify({ ok: true, status: 200 }).outcome, 'done')
|
||||
})
|
||||
|
||||
test('town crier classify: over-cap / auth / protocol errors are terminal (no retry)', () => {
|
||||
for (const status of [400, 401, 409]) {
|
||||
assert.equal(townCrier.classify({ ok: false, status }).outcome, 'terminal', `status ${status}`)
|
||||
}
|
||||
})
|
||||
|
||||
test('town crier classify: shard-transient and network errors retry', () => {
|
||||
for (const status of [503, 504, 500, 0]) {
|
||||
assert.equal(townCrier.classify({ ok: false, status }).outcome, 'retry', `status ${status}`)
|
||||
}
|
||||
})
|
||||
|
||||
// The town-crier leg's tests left with module-uo in Phase 3 — it is a MODULE's
|
||||
// leg, and its text building and status classification are its business. What
|
||||
// stays here is core's announce pipeline: the backoff schedule every leg shares,
|
||||
// the parent-status rollup over all of a job's legs, and core's own Discord leg.
|
||||
// ── classifyDiscord ──────────────────────────────────────────────────────────
|
||||
test('discord classify: ok is done, every failure retries', () => {
|
||||
assert.equal(discord.classify({ ok: true, status: 200 }).outcome, 'done')
|
||||
|
||||
@@ -503,11 +503,22 @@ test('ctx exposes exactly the documented surface, and is frozen', () => {
|
||||
assert.equal(stateOf(freshLoader(tmpRoot), 'probe').state, 'registered')
|
||||
|
||||
const probe = JSON.parse(fs.readFileSync(seen, 'utf8'))
|
||||
// API 1.1.0 added activity, users and site — each because module-uo's
|
||||
// extraction needed it and none could be vendored: an admin action a module
|
||||
// performs belongs in core's one audit log, the extension slot needs the user
|
||||
// its prefix names, and §2.7 forbids a module reading core's APP_BASE_URL.
|
||||
assert.deepEqual(probe.keys, [
|
||||
'auth', 'db', 'express', 'log', 'middleware', 'moduleId', 'paths',
|
||||
'posts', 'push', 'secretBox', 'settings', 'uploads', 'validator',
|
||||
'activity', 'auth', 'db', 'express', 'log', 'middleware', 'moduleId', 'paths',
|
||||
'posts', 'push', 'secretBox', 'settings', 'site', 'uploads', 'users', 'validator',
|
||||
])
|
||||
// is core's limiter FACTORY, not a limiter: a module states its own
|
||||
// window and cap and takes the plumbing, so there is one express-rate-limit in
|
||||
// the process and one place a breach is logged. is
|
||||
// handed over whole because it is shared policy — core's /auth/me and
|
||||
// /player/account sit behind the same counter.
|
||||
assert.deepEqual(probe.middleware, [
|
||||
'accountChangeLimiter', 'noindex', 'rateLimit', 'requireAuth', 'requireRole', 'siteMode', 'validate',
|
||||
])
|
||||
assert.deepEqual(probe.middleware, ['noindex', 'requireAuth', 'requireRole', 'siteMode', 'validate'])
|
||||
assert.equal(probe.moduleId, 'probe')
|
||||
assert.equal(probe.mutable, false, 'ctx members must be frozen')
|
||||
})
|
||||
|
||||
@@ -41,17 +41,16 @@ function tryApply(owner, build) {
|
||||
|
||||
// ── Core goes through the same door ────────────────────────────────────────
|
||||
|
||||
test('registerCore registers core AND the not-yet-extracted shard content', () => {
|
||||
test('registerCore registers exactly what core owns, and nothing else', () => {
|
||||
registries.registerCore()
|
||||
|
||||
const ids = registries.allStreams().map((s) => s.id)
|
||||
// Core's one stream first, then the seven that leave with module-uo.
|
||||
assert.equal(ids[0], 'news.post')
|
||||
assert.equal(ids.length, 8)
|
||||
assert.ok(ids.includes('vendor.sale'))
|
||||
|
||||
assert.deepEqual(registries.announceLegIds(), ['discord', 'towncrier'])
|
||||
assert.equal(registries.slotFilledBy('admin.users.detail'), 'core')
|
||||
// One stream, one leg, no filled slot. Before Phase 3 this was eight streams,
|
||||
// two legs and a core-filled `admin.users.detail` — core was holding shard
|
||||
// CONTENT so the seam would be exercised on every boot before a module first
|
||||
// used it. module-uo registers all of it now, through the same door.
|
||||
assert.deepEqual(registries.allStreams().map((s) => s.id), ['news.post'])
|
||||
assert.deepEqual(registries.announceLegIds(), ['discord'])
|
||||
assert.equal(registries.slotFilledBy('admin.users.detail'), null)
|
||||
assert.equal(registries.isCoreRegistered(), true)
|
||||
})
|
||||
|
||||
@@ -63,15 +62,26 @@ test('registerCore is idempotent — a second call registers nothing twice', ()
|
||||
})
|
||||
|
||||
test('the wire shape of a stream survives registration', () => {
|
||||
registries.registerCore()
|
||||
const personal = registries.allStreams().find((s) => s.id === 'vendor.sale')
|
||||
// Asserted against a REGISTERED stream rather than a core one, because the
|
||||
// shape is what a module hands over and core republishes. `vendor.sale` used
|
||||
// to be the subject here and is module-uo's now; a synthetic registration
|
||||
// tests the same contract without core needing a personal stream of its own.
|
||||
assert.equal(tryApply('uo', (api) => api.registerNotificationStreams([{
|
||||
id: 'uo.vendorsale',
|
||||
label: 'Vendor sales',
|
||||
description: 'One of your vendors sold something.',
|
||||
personal: true,
|
||||
requiresLinkedAccount: true,
|
||||
}])), null)
|
||||
|
||||
const personal = registries.allStreams().find((s) => s.id === 'uo.vendorsale')
|
||||
// Two booleans, not the contract's single `scope`: this object is the body of
|
||||
// GET /auth/me/notifications/streams and a shipped Android client reads both.
|
||||
assert.equal(personal.personal, true)
|
||||
assert.equal(personal.requiresLinkedAccount, true)
|
||||
assert.ok(personal.description.length > 0)
|
||||
assert.ok(registries.personalStreams().has('vendor.sale'))
|
||||
assert.equal(registries.isValidStream('vendor.sale'), true)
|
||||
assert.ok(registries.personalStreams().has('uo.vendorsale'))
|
||||
assert.equal(registries.isValidStream('uo.vendorsale'), true)
|
||||
assert.equal(registries.isValidStream('nope.nope'), false)
|
||||
})
|
||||
|
||||
@@ -185,10 +195,12 @@ test('only a declared slot can be filled, and only once', () => {
|
||||
assert.throws(() => api.registerExtension('admin.invented', router), /unknown extension slot/)
|
||||
assert.throws(() => api.registerExtension('admin.users.detail', 'not a router'), /is not a router/)
|
||||
|
||||
registries.registerCore() // core fills it
|
||||
// Core no longer fills it — module-uo does. Two registrants racing for the
|
||||
// same slot is still the case worth testing, so the first fill is a module's.
|
||||
assert.equal(tryApply('uo', (a) => a.registerExtension('admin.users.detail', router)), null)
|
||||
assert.match(
|
||||
tryApply('uo', (a) => a.registerExtension('admin.users.detail', router)),
|
||||
/already filled by "core"/,
|
||||
tryApply('rust', (a) => a.registerExtension('admin.users.detail', router)),
|
||||
/already filled by "uo"/,
|
||||
)
|
||||
})
|
||||
|
||||
@@ -208,9 +220,11 @@ test('the filled slot’s router is findable in the live app, at the resource pa
|
||||
const { findMountPrefix } = require('../swagger/slotSpecs')
|
||||
/* eslint-enable global-require */
|
||||
|
||||
const [slot] = registries.filledSlots()
|
||||
assert.ok(slot, 'app.js must have registered core, filling the slot')
|
||||
assert.equal(slot.slot, 'admin.users.detail')
|
||||
assert.ok(slot.specFile, 'core names the file its slot router is generated from')
|
||||
assert.equal(findMountPrefix(app._router.stack, slot.router), '/api/v1/admin/users/:id')
|
||||
// The slot is DECLARED by core and filled by whichever module is installed —
|
||||
// none, in core's own test run. What must keep working regardless is the
|
||||
// recovery of its mount prefix from the live stack, because that is what
|
||||
// slotSpecs.js needs and what fails silently when it breaks.
|
||||
const router = registries.declaredSlotRouter('admin.users.detail')
|
||||
assert.ok(router, 'core declares the slot at require time, filled or not')
|
||||
assert.equal(findMountPrefix(app._router.stack, router), '/api/v1/admin/users/:id')
|
||||
})
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
// 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 shardLinks model are all stubbed, so no query runs.
|
||||
// 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'
|
||||
|
||||
@@ -11,7 +11,7 @@ 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 shardLinks = require('../src/model/shardLinks/shardLinks.model')
|
||||
const appeals = require('../src/model/appeals/appeals.model')
|
||||
const db = require('../src/utils/db')
|
||||
|
||||
after(() => db.close())
|
||||
@@ -21,14 +21,14 @@ const originals = {
|
||||
isSessionRevoked: sessionService.isSessionRevoked,
|
||||
sessionMeta: sessionService.sessionMeta,
|
||||
getById: users.getById,
|
||||
listForUser: shardLinks.listForUser,
|
||||
listMine: appeals.listMine,
|
||||
}
|
||||
afterEach(() => {
|
||||
sessionService.validateSession = originals.validateSession
|
||||
sessionService.isSessionRevoked = originals.isSessionRevoked
|
||||
sessionService.sessionMeta = originals.sessionMeta
|
||||
users.getById = originals.getById
|
||||
shardLinks.listForUser = originals.listForUser
|
||||
appeals.listMine = originals.listMine
|
||||
})
|
||||
|
||||
// Sign every request in as the given DB user (role decides the gate outcome).
|
||||
@@ -44,28 +44,28 @@ function signInAs(user) {
|
||||
// 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/shard/accounts is reachable by an authenticated ${role}`, async () => {
|
||||
test(`GET /player/appeals is reachable by an authenticated ${role}`, async () => {
|
||||
signInAs({ id: 7, username: 'u', role, status: 'active' })
|
||||
shardLinks.listForUser = async (id) => {
|
||||
appeals.listMine = async (id) => {
|
||||
assert.equal(id, 7) // self-scoped to the caller regardless of role
|
||||
return [{ account: 'acctA' }]
|
||||
return [{ id: 1 }]
|
||||
}
|
||||
const app = await startApp((a) => a.use('/api/v1/player', playerRouter))
|
||||
try {
|
||||
const res = await fetch(app.url + '/api/v1/player/shard/accounts')
|
||||
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(), [{ account: 'acctA' }])
|
||||
assert.deepEqual(await res.json(), [{ id: 1 }])
|
||||
} finally {
|
||||
await app.close()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
test('GET /player/shard/accounts still rejects an unauthenticated caller with 401', async () => {
|
||||
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/shard/accounts')
|
||||
const res = await fetch(app.url + '/api/v1/player/appeals')
|
||||
assert.equal(res.status, 401)
|
||||
} finally {
|
||||
await app.close()
|
||||
@@ -76,7 +76,7 @@ test('a disabled account is still rejected with 403 (status gate, not role)', as
|
||||
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/shard/accounts')
|
||||
const res = await fetch(app.url + '/api/v1/player/appeals')
|
||||
assert.equal(res.status, 403)
|
||||
} finally {
|
||||
await app.close()
|
||||
|
||||
@@ -6,11 +6,9 @@ process.env.DB_PORT = '59999'
|
||||
const { test, after } = require('node:test')
|
||||
const assert = require('node:assert/strict')
|
||||
|
||||
const { mapShardEvent, createTracker } = require('../src/config/shardStreams')
|
||||
const pushDispatch = require('../src/utils/pushDispatch')
|
||||
// fromShardEvent moved out of pushDispatch in PR 4: core publishes, the shard
|
||||
// side resolves an event to a stream and an owner (MODULE_SYSTEM.md §1.8).
|
||||
const shardPush = require('../src/utils/shardPush')
|
||||
const db = require('../src/utils/db')
|
||||
|
||||
after(() => db.close())
|
||||
@@ -33,71 +31,11 @@ function withEnv(vars, fn) {
|
||||
}
|
||||
}
|
||||
|
||||
// ── mapShardEvent ───────────────────────────────────────────────────────────
|
||||
|
||||
test('server.hello / shutdown / crashed map to the public server.status stream', () => {
|
||||
const t = createTracker()
|
||||
assert.deepEqual(mapShardEvent({ kind: 'server.hello', bootId: 'b1' }, t), [
|
||||
{ streamId: 'server.status', ref: 'up:b1' },
|
||||
])
|
||||
assert.deepEqual(mapShardEvent({ kind: 'server.shutdown' }, t), [{ streamId: 'server.status', ref: 'down' }])
|
||||
assert.deepEqual(mapShardEvent({ kind: 'server.crashed' }, t), [{ streamId: 'server.status', ref: 'down' }])
|
||||
})
|
||||
|
||||
test('house.decay INTO idoc yields the public idoc.warning AND the owner-keyed house.idoc', () => {
|
||||
const t = createTracker()
|
||||
const out = mapShardEvent({ kind: 'house.decay', to: 'IDOC', serial: '0x40', ownerAcct: 'bob' }, t)
|
||||
assert.deepEqual(out, [
|
||||
{ streamId: 'idoc.warning', ref: '0x40' },
|
||||
{ streamId: 'house.idoc', ref: '0x40', ownerAccount: 'bob' },
|
||||
])
|
||||
// A non-IDOC decay stage produces nothing.
|
||||
assert.deepEqual(mapShardEvent({ kind: 'house.decay', to: 'Fairly', serial: '0x41' }, t), [])
|
||||
})
|
||||
|
||||
test('champ.update fires champ.start only on the inactive→active transition', () => {
|
||||
const t = createTracker()
|
||||
// First sight active → start.
|
||||
assert.deepEqual(mapShardEvent({ kind: 'champ.update', serial: 'c1', active: true }, t), [
|
||||
{ streamId: 'champ.start', ref: 'c1' },
|
||||
])
|
||||
// Still active → no re-fire.
|
||||
assert.deepEqual(mapShardEvent({ kind: 'champ.update', serial: 'c1', active: true }, t), [])
|
||||
// Goes inactive, then active again → fires again.
|
||||
assert.deepEqual(mapShardEvent({ kind: 'champ.update', serial: 'c1', active: false }, t), [])
|
||||
assert.deepEqual(mapShardEvent({ kind: 'champ.update', serial: 'c1', active: true }, t), [
|
||||
{ streamId: 'champ.start', ref: 'c1' },
|
||||
])
|
||||
})
|
||||
|
||||
test('city.update fires governor.election only on a real governor change, never on first sight', () => {
|
||||
const t = createTracker()
|
||||
// First sight of the city → no election (could be a reconnect snapshot).
|
||||
assert.deepEqual(mapShardEvent({ kind: 'city.update', city: 'Britain', governor: { serial: '0x1' } }, t), [])
|
||||
// Same governor → nothing.
|
||||
assert.deepEqual(mapShardEvent({ kind: 'city.update', city: 'Britain', governor: { serial: '0x1' } }, t), [])
|
||||
// New governor → election.
|
||||
assert.deepEqual(mapShardEvent({ kind: 'city.update', city: 'Britain', governor: { serial: '0x2' } }, t), [
|
||||
{ streamId: 'governor.election', ref: 'Britain' },
|
||||
])
|
||||
})
|
||||
|
||||
test('personal streams are owner-keyed and sensitive kinds never yield a public target', () => {
|
||||
const t = createTracker()
|
||||
const sale = mapShardEvent({ kind: 'vendor.sale', ownerAcct: 'bob', t: 7 }, t)
|
||||
assert.deepEqual(sale, [{ streamId: 'vendor.sale', ref: '7', ownerAccount: 'bob' }])
|
||||
|
||||
const login = mapShardEvent({ kind: 'account.login.attempt', acct: 'bob', ip: '1.2.3.4', t: 9 }, t)
|
||||
assert.deepEqual(login, [{ streamId: 'account.login', ref: '9', ownerAccount: 'bob' }])
|
||||
|
||||
// Every personal target carries an ownerAccount (never a bare public push).
|
||||
for (const target of [...sale, ...login]) assert.ok(target.ownerAccount, 'personal target must be owner-keyed')
|
||||
|
||||
// A truly sensitive, unmapped kind produces nothing at all.
|
||||
assert.deepEqual(mapShardEvent({ kind: 'cheat.fastwalk', acct: 'bob' }, t), [])
|
||||
assert.deepEqual(mapShardEvent({ kind: 'admin.audit', actor: 'staff' }, t), [])
|
||||
})
|
||||
|
||||
// `mapShardEvent` and `fromShardEvent` left with module-uo in Phase 3: which
|
||||
// shard event becomes which stream is the module's catalog, not core's
|
||||
// infrastructure. What stays here is what core owns — the SSRF guard on the
|
||||
// configured ntfy endpoint, and `publish()` delivering a content-free tickle to
|
||||
// the right subscribers.
|
||||
// ── isAllowedEndpoint (SSRF guard) ──────────────────────────────────────────
|
||||
|
||||
test('isAllowedEndpoint pins the configured ntfy origin and rejects everything else', () => {
|
||||
@@ -188,51 +126,3 @@ test('publish skips endpoints that fail the SSRF guard', async () => {
|
||||
assert.equal(calls[0].url, 'https://relay.test/ok')
|
||||
})
|
||||
})
|
||||
|
||||
// ── fromShardEvent (owner resolution) ───────────────────────────────────────
|
||||
|
||||
test('fromShardEvent resolves a personal event to the owning user, or drops it if unlinked', async () => {
|
||||
await withEnv({ NTFY_BASE_URL: undefined, NTFY_ALLOWED_ORIGINS: undefined }, async () => {
|
||||
const { calls, fetchImpl } = captureFetch()
|
||||
const userStreamCalls = []
|
||||
const shardLinks = { getByAccount: async (acct) => (acct === 'mine' ? { userId: 42 } : null) }
|
||||
const pushDevices = {
|
||||
endpointsForStream: async () => [],
|
||||
endpointsForUserStream: async (userId, s) => {
|
||||
userStreamCalls.push([userId, s])
|
||||
return [{ endpoint: 'https://relay.test/UPc' }]
|
||||
},
|
||||
}
|
||||
const deps = { shardLinks, pushDevices, fetchImpl, tracker: createTracker() }
|
||||
|
||||
await shardPush.fromShardEvent({ kind: 'vendor.sale', ownerAcct: 'mine', t: 1 }, deps)
|
||||
assert.deepEqual(userStreamCalls, [[42, 'vendor.sale']])
|
||||
assert.equal(calls.length, 1)
|
||||
|
||||
// Unlinked account → nobody to notify → no publish.
|
||||
userStreamCalls.length = 0
|
||||
calls.length = 0
|
||||
await shardPush.fromShardEvent({ kind: 'vendor.sale', ownerAcct: 'stranger', t: 2 }, deps)
|
||||
assert.equal(userStreamCalls.length, 0)
|
||||
assert.equal(calls.length, 0)
|
||||
})
|
||||
})
|
||||
|
||||
test('fromShardEvent fans a public shard event to the stream’s subscribers', async () => {
|
||||
await withEnv({ NTFY_BASE_URL: undefined, NTFY_ALLOWED_ORIGINS: undefined }, async () => {
|
||||
const { fetchImpl } = captureFetch()
|
||||
const publicCalls = []
|
||||
const pushDevices = {
|
||||
endpointsForStream: async (s) => {
|
||||
publicCalls.push(s)
|
||||
return []
|
||||
},
|
||||
endpointsForUserStream: async () => [],
|
||||
}
|
||||
await shardPush.fromShardEvent(
|
||||
{ kind: 'server.hello', bootId: 'b1' },
|
||||
{ pushDevices, fetchImpl, tracker: createTracker() },
|
||||
)
|
||||
assert.deepEqual(publicCalls, ['server.status'])
|
||||
})
|
||||
})
|
||||
|
||||
@@ -59,7 +59,11 @@ test('every /admin, /player and /settings route still sits behind the shared aut
|
||||
// unambiguous.
|
||||
const AUTHENTICATED_GROUPS = ['/api/v1/admin/', '/api/v1/player/', '/api/v1/settings/']
|
||||
const gated = collected.public.filter((r) => AUTHENTICATED_GROUPS.some((p) => r.path.startsWith(p)))
|
||||
assert.ok(gated.length > 100, 'expected the gated surface to be found')
|
||||
// A floor, not a count: it exists so a filter that silently matches nothing
|
||||
// fails loudly rather than passing vacuously. It was >100 before Phase 3 moved
|
||||
// 70 UO routes into module-uo, and there is no value in tracking core's exact
|
||||
// total here — the per-route assertion below is what actually guards the gate.
|
||||
assert.ok(gated.length > 50, 'expected the gated surface to be found')
|
||||
for (const route of gated) {
|
||||
assert.ok(
|
||||
route.gates.includes('requireAuth'),
|
||||
|
||||
Reference in New Issue
Block a user