import { randomUUID } from 'node:crypto'; import Database from 'better-sqlite3'; import { afterEach, beforeEach, expect, it, vi } from 'vitest'; import { newFixture, type Fixture } from '../helpers/fixture.js'; let fixture: Fixture; let handle: Database.Database; const clock = '2026-10-17T12:00:00.000Z'; beforeEach(async () => { vi.resetModules(); vi.useFakeTimers({ toFake: ['Date'] }); vi.setSystemTime(new Date(clock)); fixture = await newFixture([]); const { db } = await import('../../src/lib/server/db/index.js'); handle = db(); for (const role of ['admin', 'moderator', 'super_viewer', 'viewer']) handle .prepare( `INSERT INTO auth_users(id,username,display_name,password_hash,role,created_at,updated_at) VALUES(?,?,?,'unused',?,0,0)` ) .run(role, role, role, role); }); afterEach(async () => { vi.useRealTimers(); await fixture.cleanup(); }); async function mutate(input: Record, actor = 'moderator') { const { mutateModeration } = await import('../../src/lib/server/moderation/actions.js'); return mutateModeration(actor, { idempotencyKey: randomUUID(), ...input }); } async function detail(id: string, actor = 'moderator') { const { moderationCaseDetail } = await import('../../src/lib/server/moderation/cases.js'); return moderationCaseDetail(actor, id); } const create = (overrides: Record = {}) => mutate({ op: 'create-case', game: 't5', guid: '123', title: 'Review conduct', ...overrides }); const restriction = (overrides: Record = {}, actor = 'moderator') => mutate( { op: 'create-sanction', game: 't5', guid: '123', kind: 'mute', scope: 'game', durationMinutes: 60, reason: 'Earlier moderation action', ...overrides }, actor ); function action(overrides: Record = {}) { const record = { id: randomUUID(), game: 't5', guid: '123', kind: 'warn', state: 'confirmed', ...overrides }; handle .prepare( `INSERT INTO moderation_actions(id,game,guid,server,instance_id,generation,session, kind,reason,actor_id,actor,created_at,expires_at,state) VALUES(?,?,?,'retired-server','retired-instance',1,'old-session',?, 'Earlier warning','moderator','Former moderator',?,?,?)` ) .run(record.id, record.game, record.guid, record.kind, clock, clock, record.state); handle .prepare( `INSERT INTO moderation_events(game,guid,actor,action,subject_id,detail,created_at) VALUES(?,?,'Former moderator',?,?,'Recorded delivery result',?)` ) .run(record.game, record.guid, `action.${record.state}`, record.id, clock); return record.id as string; } function enforcement() { return { sanctions: handle.prepare('SELECT * FROM moderation_sanctions ORDER BY id').all(), actions: handle.prepare('SELECT * FROM moderation_actions ORDER BY id').all(), policy: handle.prepare('SELECT * FROM moderation_policy').all() }; } const link = (id: string, revision: number, actionType: string, actionId: string) => ({ op: 'link-case-action', id, revision, actionType, actionId, reason: 'This earlier action addresses the incident under review.' }); it('links a prior restriction to multiple cases without changing enforcement or its original case', async () => { const original = await create(); const sanction = await restriction({ caseId: original.id, caseRevision: 1 }); const current = await create(); const before = enforcement(); const request = { ...link(current.id, 1, 'restriction', sanction.id), idempotencyKey: randomUUID() }; await mutate(request); await mutate(request); let data = await detail(current.id); expect(data.case.revision).toBe(2); expect(data.sanctions).toMatchObject([{ id: sanction.id, caseId: original.id }]); expect(data.linkedActions).toMatchObject([ { id: sanction.id, type: 'restriction', state: 'active' } ]); expect(data.availableActions).toEqual([]); expect(data.events.map((event) => event.action)).toContain('restriction.created'); expect((await detail(original.id)).linkedActions).toMatchObject([{ id: sanction.id }]); expect(enforcement()).toEqual(before); await mutate(link(current.id, 2, 'restriction', sanction.id)); expect(await detail(current.id)).toEqual(data); expect( handle.prepare('SELECT COUNT(*) AS count FROM moderation_case_action_links').get() ).toEqual({ count: 1 }); const event = data.events.find((entry) => entry.action === 'case.action_linked')!; expect(JSON.parse(event.detail)).toEqual({ reason: request.reason, actionReference: { type: 'restriction', id: sanction.id } }); await mutate({ op: 'revoke-sanction', id: sanction.id, revision: 1, reason: 'Review completed' }); data = await detail(current.id); expect(data.case.revision).toBe(3); expect((await detail(original.id)).case.revision).toBe(3); expect(data.linkedActions?.[0].state).toBe('revoked'); expect(data.events[0].action).toBe('restriction.revoked'); }); it('links historical live actions to closed cases and exposes their real delivery state and timeline', async () => { const warningId = action(); const failedId = action({ state: 'failed' }); action({ game: 't6' }); action({ guid: '456' }); action({ guid: null, kind: 'announce' }); const { id } = await create(); expect((await detail(id)).availableActions?.map((entry) => entry.id).sort()).toEqual( [warningId, failedId].sort() ); await mutate({ op: 'resolve-case', id, revision: 1, status: 'no_action', reason: 'Initial review complete' }); const before = enforcement(); await mutate(link(id, 2, 'action', warningId)); const data = await detail(id, 'super_viewer'); expect(data.case).toMatchObject({ status: 'no_action', revision: 3 }); expect(data.linkedActions).toMatchObject([{ id: warningId, kind: 'warn', state: 'confirmed' }]); expect(data.availableActions).toMatchObject([{ id: failedId, state: 'failed' }]); expect(data.events.map((event) => event.action)).toContain('action.confirmed'); expect(enforcement()).toEqual(before); await expect(detail(id, 'viewer')).rejects.toMatchObject({ status: 403 }); }); it('rejects unauthorized, stale, missing and wrong-player or game links and permits a matching fleet restriction', async () => { const { id } = await create(); const right = action(); const otherPlayer = action({ guid: '456' }); const otherGame = action({ game: 't6' }); const wrongRestriction = await restriction({ guid: '456' }); for (const actor of ['super_viewer', 'viewer']) await expect(mutate(link(id, 1, 'action', right), actor)).rejects.toMatchObject({ status: 403 }); await expect(mutate(link(id, 2, 'action', right))).rejects.toMatchObject({ status: 409 }); for (const [type, source] of [ ['action', otherPlayer], ['action', otherGame], ['action', 'missing'], ['restriction', wrongRestriction.id] ]) await expect(mutate(link(id, 1, type, source))).rejects.toMatchObject({ status: 404 }); expect((await detail(id)).case.revision).toBe(1); expect(handle.prepare('SELECT * FROM moderation_case_action_links').all()).toEqual([]); for (const game of ['t5', 't6']) handle .prepare('INSERT INTO moderation_aliases VALUES(?,?,?,?,?)') .run(game, '123', 'Player', clock, clock); const fleet = await restriction({ scope: 'fleet' }, 'admin'); const t6 = await create({ game: 't6' }); expect((await detail(t6.id)).availableActions?.map((entry) => entry.id).sort()).toEqual( [fleet.id, otherGame].sort() ); await mutate(link(t6.id, 1, 'restriction', fleet.id)); expect((await detail(t6.id)).sanctions).toMatchObject([ { id: fleet.id, scope: 'fleet', game: 't5' } ]); }); it('requires a linked confirmed warning for a cited warning outcome and preserves the actual reference', async () => { const { id } = await create(); const confirmed = action(); const failed = action({ state: 'failed' }); const kick = action({ kind: 'kick' }); const sanction = await restriction(); const resolve = ( revision: number, type: string, actionId: string, status = 'warning_issued' ) => ({ op: 'resolve-case', id, revision, status, reason: 'Decision after evidence review', actionReference: { type, id: actionId } }); await expect(mutate(resolve(1, 'action', confirmed))).rejects.toThrow( 'Link the recorded action' ); await mutate(link(id, 1, 'action', failed)); await mutate(link(id, 2, 'action', kick)); await mutate(link(id, 3, 'restriction', sanction.id)); for (const [type, source] of [ ['action', failed], ['action', kick], ['restriction', sanction.id] ]) await expect(mutate(resolve(4, type, source))).rejects.toThrow('confirmed warning'); await mutate(link(id, 4, 'action', confirmed)); await expect(mutate(resolve(5, 'action', confirmed, 'restriction_applied'))).rejects.toThrow( 'recorded restriction' ); const before = enforcement(); await mutate(resolve(5, 'action', confirmed)); const data = await detail(id); expect(data.case).toMatchObject({ status: 'warning_issued', revision: 6 }); expect(JSON.parse(data.events[0].detail)).toEqual({ reason: 'Decision after evidence review', actionReference: { type: 'action', id: confirmed } }); expect(enforcement()).toEqual(before); }); it('allows a historical expired restriction created from the original case to support its closure', async () => { const { id } = await create(); const sanction = await restriction({ caseId: id, caseRevision: 1 }); vi.setSystemTime(new Date(Date.parse(clock) + 120 * 60_000)); expect((await detail(id)).linkedActions).toMatchObject([{ id: sanction.id, state: 'expired' }]); const before = enforcement(); await mutate({ op: 'resolve-case', id, revision: 2, status: 'restriction_applied', reason: 'Earlier mute addressed the incident', actionReference: { type: 'restriction', id: sanction.id } }); expect((await detail(id)).case.status).toBe('restriction_applied'); expect(enforcement()).toEqual(before); }); it('rolls back a link, case revision and idempotency record if its audit write fails', async () => { const { id } = await create(); const warning = action(); const before = await detail(id); handle.exec(`CREATE TRIGGER reject_case_action_audit BEFORE INSERT ON moderation_events WHEN NEW.action='case.action_linked' BEGIN SELECT RAISE(ABORT,'audit unavailable'); END`); const request = { ...link(id, 1, 'action', warning), idempotencyKey: randomUUID() }; await expect(mutate(request)).rejects.toThrow('audit unavailable'); expect(await detail(id)).toEqual(before); expect(handle.prepare('SELECT * FROM moderation_case_action_links').all()).toEqual([]); expect( handle .prepare('SELECT * FROM moderation_requests WHERE request_key=?') .all(request.idempotencyKey) ).toEqual([]); handle.exec('DROP TRIGGER reject_case_action_audit'); await mutate(request); expect((await detail(id)).linkedActions).toMatchObject([{ id: warning }]); }); it('upgrades existing databases without changing original case restrictions and enforces link identity', async () => { const { migrations } = await import('../../src/lib/server/db/migrations.js'); const { runMigrations } = await import('../../src/lib/server/db/migration-runner.js'); const legacy = new Database(':memory:'); legacy.pragma('foreign_keys = ON'); try { runMigrations(legacy, migrations.slice(0, 30)); legacy .prepare( `INSERT INTO moderation_cases(id,game,guid,title,status,actor_id,actor,created_at,updated_at) VALUES('case','t5','123','Prior case','open','former','Former staff',?,?)` ) .run(clock, clock); legacy .prepare( `INSERT INTO moderation_sanctions(id,game,guid,kind,scope,reason,note,actor_id,actor,created_at,case_id) VALUES('restriction','t5','123','mute','game','Prior restriction','','former','Former staff',?,'case')` ) .run(clock); const before = legacy.prepare('SELECT * FROM moderation_sanctions').all(); runMigrations(legacy, migrations); runMigrations(legacy, migrations); expect(legacy.prepare('SELECT * FROM moderation_sanctions').all()).toEqual(before); const insert = legacy.prepare('INSERT INTO moderation_case_action_links VALUES(?,?,?,?,?)'); insert.run('case', 'restriction', 'restriction', 'Moderator', clock); expect(() => insert.run('case', 'restriction', 'restriction', 'Moderator', clock)).toThrow( 'UNIQUE' ); expect(() => insert.run('missing', 'action', 'action', 'Moderator', clock)).toThrow( 'FOREIGN KEY' ); expect(() => insert.run('case', 'other', 'action', 'Moderator', clock)).toThrow('CHECK'); expect(legacy.pragma('foreign_key_check')).toEqual([]); } finally { legacy.close(); } });