Players whose linked Discord identity was banned or muted can now submit an appeal from the portal and track it; staff get a queue in the admin moderation section to claim and resolve (approve/deny) appeals. Approving a ban/mute appeal best-effort asks the Discord bot to reverse the action (unban / clear timeout) via the internal API and posts a mod-log embed; a down bot never fails the resolution (reversal_status is recorded). - Schema: new server-owned `appeals` table (no cross-owner FK to mod_actions; existence validated in app code). - Server: model/appeals/* + player appeals controller (submit/mine/ eligible/withdraw) and admin queue handlers (list/claim/resolve/ per-user) under the existing admin+moderator gate; one-active-appeal enforced app-side; eligibility keyed on the caller's linked Discord id. - 6d: bot POST /internal/mod-reverse (+ modLog.postReversal) and server botInternalClient.reverseModAction, wired into resolve(). - Client: admin Appeals queue + resolve modal, ModerationUser appeals tab, player Appeals page (submit/withdraw), nav + routes + api methods. - Docs: swagger annotations + component schemas, regenerated output. - Tests: appeals controller + pure suites (server npm test 224 green). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XmHdsbnLzDMAVQkAoTQSBe
48 lines
2.1 KiB
JavaScript
48 lines
2.1 KiB
JavaScript
// Unit tests for the pure appeals helpers (status/type vocabularies + the
|
|
// reversal-status derivation). DB-free, like the rest of this suite.
|
|
const { test } = require('node:test')
|
|
const assert = require('node:assert/strict')
|
|
|
|
const pure = require('../src/model/appeals/appeals.pure')
|
|
|
|
test('TERMINAL / ACTIVE partition the status space', () => {
|
|
assert.deepEqual([...pure.TERMINAL].sort(), ['approved', 'denied', 'withdrawn'])
|
|
assert.deepEqual([...pure.ACTIVE].sort(), ['pending', 'under_review'])
|
|
})
|
|
|
|
test('isTerminal is true only for closed statuses', () => {
|
|
assert.equal(pure.isTerminal('approved'), true)
|
|
assert.equal(pure.isTerminal('denied'), true)
|
|
assert.equal(pure.isTerminal('withdrawn'), true)
|
|
assert.equal(pure.isTerminal('pending'), false)
|
|
assert.equal(pure.isTerminal('under_review'), false)
|
|
})
|
|
|
|
test('isActive is true only for the open statuses that hold the appeal slot', () => {
|
|
assert.equal(pure.isActive('pending'), true)
|
|
assert.equal(pure.isActive('under_review'), true)
|
|
assert.equal(pure.isActive('approved'), false)
|
|
assert.equal(pure.isActive('withdrawn'), false)
|
|
})
|
|
|
|
test('isAppealableType allows only ban and mute', () => {
|
|
assert.equal(pure.isAppealableType('ban'), true)
|
|
assert.equal(pure.isAppealableType('mute'), true)
|
|
assert.equal(pure.isAppealableType('kick'), false)
|
|
assert.equal(pure.isAppealableType('warn'), false)
|
|
})
|
|
|
|
test('reversalStatusFor: approved + appealable maps bot success to done/failed', () => {
|
|
assert.equal(pure.reversalStatusFor({ status: 'approved', actionType: 'ban', botOk: true }), 'done')
|
|
assert.equal(pure.reversalStatusFor({ status: 'approved', actionType: 'ban', botOk: false }), 'failed')
|
|
assert.equal(pure.reversalStatusFor({ status: 'approved', actionType: 'mute', botOk: true }), 'done')
|
|
})
|
|
|
|
test('reversalStatusFor: denial never reverses', () => {
|
|
assert.equal(pure.reversalStatusFor({ status: 'denied', actionType: 'ban', botOk: true }), 'none')
|
|
})
|
|
|
|
test('reversalStatusFor: non-appealable action never reverses even when approved', () => {
|
|
assert.equal(pure.reversalStatusFor({ status: 'approved', actionType: 'warn', botOk: true }), 'none')
|
|
})
|