Add meaningful unit tests for three server models with untested business logic, each against an in-memory fake db (no DB required): - pages.model: slug validation + reserved-name guard, slug immutability, the protected ON-via-update / OFF-only-via-unprotect asymmetry, publish stamping, draft invisibility to public reads, dup-slug → 409, block gate. - shardState.model: partial-refresh field dropping (vitals must not clobber login fields), is_idoc derivation, economy clamp/ordering/Number coercion, presence zero-snapshot defaults, payload-fallback shaping, and the camelCase read-shaping contract the site + Android client depend on. - moderation.model: five-feed window merge, userSummary count/total semantics (total sums unknown types too), and graceful degradation when bot config is missing. Lifts: pages.model 23%→82%, moderation.model 27%→85%, shardState.model 33%→62% line coverage. Co-Authored-By: Claude <noreply@anthropic.com>
111 lines
5.0 KiB
JavaScript
111 lines
5.0 KiB
JavaScript
// 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
|
|
})
|