// Point the DB pool at a dead port before it's built; the db modules are // monkeypatched below, and pool.close() at the end lets the process exit cleanly. process.env.DB_HOST = '127.0.0.1' process.env.DB_PORT = '59999' const { test, beforeEach, afterEach, after } = require('node:test') const assert = require('node:assert/strict') const pool = require('../src/utils/db') after(() => pool.close()) // Unit-test the moderation MODEL's orchestration (the pure helpers are covered // separately in moderation.test.js). What the model adds on top: // - summary() merges five independent window feeds (mod actions + member // joins/leaves + invite joins + filter/spam hits) into one windows object; // - userSummary() zero-fills per-type counts but total_actions sums EVERY row, // including action types not in the fixed count set; // - a missing/failing bot config degrades to "nothing automated", never throws. const moderationDb = require('../src/model/moderation/moderation.db') const botConfigDb = require('../src/model/botConfig/botConfig.db') const moderation = require('../src/model/moderation/moderation.model') const saved = {} function patch(mod, name, fn) { saved[`${mod === moderationDb ? 'm' : 'b'}:${name}`] = mod[name] mod[name] = fn } afterEach(() => { for (const key of Object.keys(saved)) { const [tag, name] = key.split(':') const mod = tag === 'm' ? moderationDb : botConfigDb mod[name] = saved[key] delete saved[key] } }) // ── summary() merges every feed into the three windows ────────────────── test('summary folds mod-action counts and all four event feeds into each window', async () => { patch(moderationDb, 'countsByWindow', async () => [{ action_type: 'ban', d1: 1, d7: 2, d30: 3 }]) patch(moderationDb, 'memberCountsByWindow', async () => [ { event_type: 'join', d1: 5, d7: 6, d30: 7 }, { event_type: 'leave', d1: 1, d7: 1, d30: 1 }, ]) patch(moderationDb, 'inviteJoinCountsByWindow', async () => ({ d1: 2, d7: 2, d30: 2 })) patch(moderationDb, 'tableCountsByWindow', async (table) => table === 'filter_hits' ? { d1: 9, d7: 9, d30: 9 } : { d1: 4, d7: 4, d30: 4 }, ) const { windows } = await moderation.summary() assert.equal(windows['24h'].ban, 1) assert.equal(windows['7d'].ban, 2) assert.equal(windows['24h'].joins, 5) assert.equal(windows['24h'].leaves, 1) assert.equal(windows['24h'].invite_joins, 2) assert.equal(windows['24h'].filter_hits, 9) assert.equal(windows['24h'].spam_hits, 4) assert.equal(windows['30d'].joins, 7) }) test('summary zero-fills a feed that returned no rows for a window', async () => { patch(moderationDb, 'countsByWindow', async () => []) patch(moderationDb, 'memberCountsByWindow', async () => []) // no join/leave rows patch(moderationDb, 'inviteJoinCountsByWindow', async () => null) patch(moderationDb, 'tableCountsByWindow', async () => null) const { windows } = await moderation.summary() assert.equal(windows['24h'].joins, 0) assert.equal(windows['7d'].filter_hits, 0) assert.equal(windows['30d'].ban, 0) }) // ── userSummary() count/total semantics ───────────────────────────────── test('userSummary zero-fills known types but total_actions sums every row', async () => { patch(moderationDb, 'userCounts', async () => [ { action_type: 'ban', c: '2' }, { action_type: 'warn', c: 3 }, { action_type: 'note', c: 5 }, // NOT in zeroCounts — excluded from counts, still in total ]) patch(moderationDb, 'latestTag', async () => 'Griefer#1') patch(moderationDb, 'linkedAccount', async () => ({ id: 9, username: 'griefer' })) const out = await moderation.userSummary('123') assert.equal(out.counts.ban, 2) assert.equal(out.counts.warn, 3) assert.equal(out.counts.kick, 0) // zero-filled assert.equal(out.counts.note, undefined) // unknown type not surfaced as a count assert.equal(out.total_actions, 10) // 2 + 3 + 5 — total includes the unknown type assert.equal(out.tag, 'Griefer#1') assert.deepEqual(out.linked_account, { id: 9, username: 'griefer' }) }) // ── automated-action annotation depends on bot config, which may be absent ── test('recent flags actions taken by the bot application id as automated', async () => { patch(botConfigDb, 'get', async () => ({ application_id: '999' })) patch(moderationDb, 'recentActions', async () => [ { id: 1, staff_user_id: '999' }, // the bot { id: 2, staff_user_id: '42' }, // a human mod ]) const rows = await moderation.recent({}) assert.equal(rows[0].is_automated, true) assert.equal(rows[1].is_automated, false) }) test('recent degrades to nothing-automated when bot config lookup throws', async () => { patch(botConfigDb, 'get', async () => { throw new Error('bot config table missing') }) patch(moderationDb, 'recentActions', async () => [{ id: 1, staff_user_id: '999' }]) const rows = await moderation.recent({}) assert.equal(rows[0].is_automated, false) // appId resolved to null, not a crash })