feat(teams): phase 5 — Forum 5b, discussion + moderation + reports #155

Merged
whitlocktech merged 5 commits from feature/teams-phase5-discussion into edge 2026-08-18 18:36:52 +00:00
2 changed files with 34 additions and 1 deletions
Showing only changes of commit c970caee16 - Show all commits

View File

@@ -176,7 +176,15 @@ forumRouter.post(
/* #swagger.responses[400] = { description: 'An action that applies to a thread, not a post', content: { "application/json": { schema: { $ref: "#/components/schemas/Error" } } } } */ /* #swagger.responses[400] = { description: 'An action that applies to a thread, not a post', content: { "application/json": { schema: { $ref: "#/components/schemas/Error" } } } } */
/* #swagger.responses[403] = { description: 'Not a leader of this Team', content: { "application/json": { schema: { $ref: "#/components/schemas/Error" } } } } */ /* #swagger.responses[403] = { description: 'Not a leader of this Team', content: { "application/json": { schema: { $ref: "#/components/schemas/Error" } } } } */
param('id').isInt({ min: 1 }).toInt(), param('id').isInt({ min: 1 }).toInt(),
body('action').isIn(['hide', 'unhide', 'delete', 'restore']), // **Deliberately the FULL action list, not the four a post accepts.** The model
// answers `pin` with "that applies to a thread, not to a post" and an invented
// action with "unknown", and a validator that allowed only the four would turn
// the first of those into a generic "Validation failed" — leaving the precise
// message reachable only from a unit test. Found on the live rig, where `pin`
// came back as a validation error rather than as the sentence written for it.
// Both are 400 and neither is a security boundary; the difference is entirely
// whether the caller is told which mistake they made.
body('action').isIn(['pin', 'unpin', 'lock', 'unlock', 'hide', 'unhide', 'delete', 'restore']),
body('reason').optional().isString().trim().isLength({ max: 255 }), body('reason').optional().isString().trim().isLength({ max: 255 }),
validate, validate,
ctrl.moderatePost, ctrl.moderatePost,

View File

@@ -495,3 +495,28 @@ test('the grant routes answer even while the forum is switched off', async () =>
assert.equal((await get(app, '/api/v1/player/teams/a/grants')).status, 200) assert.equal((await get(app, '/api/v1/player/teams/a/grants')).status, 200)
}) })
}) })
test('pin on a POST reaches the model, so the caller is told which mistake they made', async () => {
// The route's validator deliberately accepts all eight actions. Narrowing it to
// the four a post takes would turn "that applies to a thread, not to a post"
// into a generic "Validation failed" — the precise message would exist, be
// unit-tested, and be unreachable through the API. Found on the live rig.
signInAs(admin)
patch(forumSettings, 'forumsEnabled', async () => true)
patch(teamsDbModule, 'findBySlug', async () => ({ id: 1, name: 'A' }))
patch(access, 'forumAccess', async () => ({ allowed: true, viaMembership: true, viaGrant: false, isLeader: false }))
patch(forum, 'moderatePost', async ({ action }) => ({
ok: false, status: 400, error: `"${action}" applies to a thread, not to a post`,
}))
await withApp('/api/v1/player', playerRouter, async (app) => {
const res = await post(app, '/api/v1/player/teams/a/forum/posts/1/moderate', { action: 'pin' })
assert.equal(res.status, 400)
assert.match((await res.json()).message, /applies to a thread/)
// An action that is not in the enum at all still stops at the validator —
// widening the list is not the same as removing it.
const nonsense = await post(app, '/api/v1/player/teams/a/forum/posts/1/moderate', { action: 'incinerate' })
assert.equal(nonsense.status, 400)
})
})