#233 left this open: the forum hid a blocked author's posts from a member, but the member was still pushed, put in the inbox and emailed about them. - teamNotify.forumPost excludes every member who blocks the post's author, or the member who started the thread (the forum 404s that thread for them). One recipient list, so push, the inbox and email skip the same people. userSafety.blockersOf answers it, and a staff author is blocked by nobody (S8). A failed lookup notifies without it. - The daily digest applies each recipient's forumFilter at send time: blocked authors, blocked thread starters, hidden threads and posts. digestPostsSince now also returns the thread's created_by. - A reply's notify payload carries threadAuthorUserId. The Team activity feed needed nothing: forum posts are never written to it (membership, renames and module game events only). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01E14m6SuuY6i1vASFeGDBeY
165 lines
8.2 KiB
JavaScript
165 lines
8.2 KiB
JavaScript
// ── Who a deleted account's snapshots show as (ACCOUNT_DELETION.md AD5, AD6) ──
|
||
//
|
||
// The rule itself, then each place a member can read a snapshot: forum threads
|
||
// and posts, the leader's guest list, and the Team digest mail. The Reports
|
||
// queue (staff-only) is covered in contentReports.test.js.
|
||
process.env.DB_HOST = '127.0.0.1'
|
||
process.env.DB_PORT = '59999'
|
||
|
||
const { test, afterEach, after } = require('node:test')
|
||
const assert = require('node:assert/strict')
|
||
|
||
const { displayAuthor, DELETED_PLAYER, seesDeletedNames, hasActor } = require('../src/utils/displayAuthor')
|
||
const forum = require('../src/model/teams/teamForum.model')
|
||
const grants = require('../src/model/teams/teamGrants.model')
|
||
const accessDb = require('../src/model/teams/teamAccess.db')
|
||
const teamsDb = require('../src/model/teams/teams.db')
|
||
const db = require('../src/utils/db')
|
||
|
||
after(() => db.close())
|
||
|
||
const saved = []
|
||
function patch(obj, key, value) {
|
||
saved.push([obj, key, obj[key]])
|
||
obj[key] = value
|
||
}
|
||
afterEach(() => {
|
||
while (saved.length) {
|
||
const [obj, key, value] = saved.pop()
|
||
obj[key] = value
|
||
}
|
||
})
|
||
|
||
const MEMBER = { role: 'player' }
|
||
const STAFF = ['admin', 'editor', 'moderator'].map((role) => ({ role }))
|
||
|
||
// ── the rule ───────────────────────────────────────────────────────────────
|
||
|
||
test('a live account shows its name to everyone', () => {
|
||
for (const viewer of [null, MEMBER, ...STAFF]) {
|
||
assert.deepEqual(displayAuthor({ userId: 4, username: 'Thorne' }, viewer), { name: 'Thorne', deleted: false })
|
||
}
|
||
})
|
||
|
||
test('a deleted account reads "[deleted player]" to members and visitors (AD5)', () => {
|
||
assert.equal(DELETED_PLAYER, '[deleted player]')
|
||
for (const viewer of [null, undefined, MEMBER, { role: 'leader' }, {}]) {
|
||
assert.deepEqual(displayAuthor({ userId: null, username: 'Ravenna' }, viewer), { name: DELETED_PLAYER, deleted: true })
|
||
}
|
||
})
|
||
|
||
test('staff — admin, editor and moderator — see the stored name, marked deleted (AD6)', () => {
|
||
for (const viewer of STAFF) {
|
||
assert.equal(seesDeletedNames(viewer), true)
|
||
assert.deepEqual(displayAuthor({ userId: null, username: 'Ravenna' }, viewer), { name: 'Ravenna', deleted: true })
|
||
}
|
||
// With no snapshot at all there is no name to show anybody.
|
||
assert.deepEqual(displayAuthor({ userId: null, username: null }, STAFF[0]), { name: DELETED_PLAYER, deleted: true })
|
||
})
|
||
|
||
test('an absent actor is not a deleted one', () => {
|
||
assert.equal(hasActor(null, null), false)
|
||
assert.equal(hasActor(null, ''), false)
|
||
assert.equal(hasActor(null, 'OldMod'), true)
|
||
assert.equal(hasActor(5, null), true)
|
||
})
|
||
|
||
// ── forum threads and posts ────────────────────────────────────────────────
|
||
|
||
const threadRow = (over = {}) => ({
|
||
id: 5, type: 'discussion', title: 'Raid night', created_by: null, created_username: 'Ravenna',
|
||
created_at: new Date(), last_post_at: new Date(), post_count: 2, pinned: 0, locked: 0, status: 'visible', ...over,
|
||
})
|
||
const postRow = (over = {}) => ({
|
||
id: 80, thread_id: 5, author_user_id: null, author_username: 'Ravenna', body_html: '<p>Guide</p>',
|
||
created_at: new Date(), edited_at: null, status: 'visible', ...over,
|
||
})
|
||
|
||
test('a forum thread by a deleted account: "[deleted player]" to a member, the name to staff', () => {
|
||
assert.equal(forum.publicThread(threadRow(), MEMBER).author, DELETED_PLAYER)
|
||
assert.equal(forum.publicThread(threadRow()).author, DELETED_PLAYER) // no viewer said
|
||
assert.equal(forum.publicThread(threadRow(), MEMBER).authorDeleted, true)
|
||
for (const viewer of STAFF) {
|
||
const t = forum.publicThread(threadRow(), viewer)
|
||
assert.equal(t.author, 'Ravenna')
|
||
assert.equal(t.authorDeleted, true)
|
||
}
|
||
assert.equal(forum.publicThread(threadRow({ created_by: 4 }), MEMBER).author, 'Ravenna')
|
||
assert.equal(forum.publicThread(threadRow({ created_by: 4 }), MEMBER).authorDeleted, false)
|
||
})
|
||
|
||
test('a forum post by a deleted account follows the same rule, and is nobody’s to edit', () => {
|
||
const asMember = forum.renderPost(postRow(), 'disabled', { userId: 9, role: 'player', isStaff: false })
|
||
assert.equal(asMember.author, DELETED_PLAYER)
|
||
assert.equal(asMember.authorDeleted, true)
|
||
assert.equal(asMember.mine, false)
|
||
assert.equal(asMember.canEdit, false)
|
||
|
||
const asEditor = forum.renderPost(postRow(), 'disabled', { userId: 2, role: 'editor', isStaff: false })
|
||
assert.equal(asEditor.author, 'Ravenna')
|
||
assert.equal(asEditor.authorDeleted, true)
|
||
})
|
||
|
||
test('the thread list and a thread pass the viewer down to every row', async () => {
|
||
const forumDb = require('../src/model/teams/teamForum.db')
|
||
const forumSettings = require('../src/model/teams/teamForumSettings.model')
|
||
patch(forumDb, 'threadsByTeam', async () => [threadRow()])
|
||
patch(forumDb, 'threadById', async () => ({ ...threadRow(), team_id: 1 }))
|
||
patch(forumDb, 'postsByThread', async () => [postRow()])
|
||
patch(forumSettings, 'imageMode', async () => 'disabled')
|
||
|
||
const [listedForMember] = await forum.listThreads(1, { viewer: MEMBER })
|
||
assert.equal(listedForMember.author, DELETED_PLAYER)
|
||
const [listedForStaff] = await forum.listThreads(1, { viewer: { role: 'moderator' } })
|
||
assert.equal(listedForStaff.author, 'Ravenna')
|
||
|
||
const thread = await forum.getThread(1, 5, { viewer: { userId: 9, role: 'player' } })
|
||
assert.equal(thread.author, DELETED_PLAYER)
|
||
assert.equal(thread.posts[0].author, DELETED_PLAYER)
|
||
const staffThread = await forum.getThread(1, 5, { canModerate: true, viewer: { userId: 2, role: 'admin', isStaff: true } })
|
||
assert.equal(staffThread.author, 'Ravenna')
|
||
assert.equal(staffThread.posts[0].author, 'Ravenna')
|
||
})
|
||
|
||
// ── the leader's guest list ────────────────────────────────────────────────
|
||
|
||
test('a deleted account is not a forum guest, and a deleted granter reads "[deleted player]" (AD13)', async () => {
|
||
patch(accessDb, 'activeGrants', async () => [
|
||
{ user_id: 21, username: 'Guest', granted_by: null, granted_username: 'OldLeader', granted_at: new Date(), reason: null },
|
||
{ user_id: null, username: 'Ravenna', granted_by: 4, granted_username: 'Leader', granted_at: new Date(), reason: null },
|
||
{ user_id: 22, username: 'Member', granted_by: 4, granted_username: 'Leader', granted_at: new Date(), reason: null },
|
||
])
|
||
patch(teamsDb, 'membersByTeam', async () => [{ user_id: 22 }])
|
||
|
||
const guests = await grants.forumGuests(1, MEMBER)
|
||
assert.deepEqual(guests.map((g) => g.username), ['Guest'])
|
||
assert.equal(guests[0].grantedBy, DELETED_PLAYER)
|
||
|
||
const forStaff = await grants.forumGuests(1, { role: 'moderator' })
|
||
assert.equal(forStaff[0].grantedBy, 'OldLeader')
|
||
})
|
||
|
||
// ── the Team digest ────────────────────────────────────────────────────────
|
||
|
||
test('the digest mail names a deleted author "[deleted player]"', async () => {
|
||
const worker = require('../src/utils/teamDigestWorker')
|
||
const teamNotify = require('../src/model/teams/teamNotify.model')
|
||
const templates = require('../src/engagement/templates')
|
||
const safety = require('../src/model/safety/userSafety.model')
|
||
let items = null
|
||
patch(safety, 'forumFilter', async () => ({
|
||
blockedUserIds: new Set(),
|
||
hidden: { team_forum_thread: new Set(), team_forum_post: new Set() },
|
||
}))
|
||
patch(teamNotify, 'digestPostsSince', async () => [
|
||
{ id: 1, thread_id: 5, body_html: '<p>a</p>', created_at: new Date(), author_user_id: null, author_username: 'Ravenna', title: 'Raid night', type: 'discussion' },
|
||
{ id: 2, thread_id: 5, body_html: '<p>b</p>', created_at: new Date(), author_user_id: 4, author_username: 'Thorne', title: 'Raid night', type: 'discussion' },
|
||
])
|
||
patch(templates, 'renderByKey', async (key, values) => {
|
||
items = values.items
|
||
return null // stop before anything is sent
|
||
})
|
||
await worker.sendOne({ id: 1, slug: 'wolves', name: 'Iron Wolves' }, { user_id: 9, last_digest_at: null }, null, new Date())
|
||
assert.deepEqual(items.map((i) => i.heading), ['Raid night — [deleted player]', 'Raid night — Thorne'])
|
||
})
|