Files
electric-cherry/tests/unit/moderation-foundation.test.ts

90 lines
3.6 KiB
TypeScript

import { afterEach, beforeEach, expect, it, vi } from 'vitest';
import { newFixture, type Fixture } from '../helpers/fixture.js';
import { moderationGuid } from '../../src/lib/moderation.js';
let fixture: Fixture;
beforeEach(async () => {
vi.resetModules();
fixture = await newFixture();
});
afterEach(async () => fixture.cleanup());
it('keeps large identities exact and normalizes only known representations', () => {
expect(moderationGuid('t6', '110000100000001', 'rcon')).toBe('76561197960265729');
expect(moderationGuid('t6', '076561197960265729')).toBe('76561197960265729');
expect(moderationGuid('t5', 'DEADbeef', 'rcon')).toBe('DEADbeef');
expect(moderationGuid('t5', 'Some:opaque_ID')).toBe('Some:opaque_ID');
});
it('preserves aliases and moderation identity independently of gameplay retention', async () => {
const { db } = await import('../../src/lib/server/db/index.js');
const { observeModerationPlayers } = await import('../../src/lib/server/moderation/core.js');
const handle = db();
observeModerationPlayers(
handle,
't6',
[{ guid: '000123', name: 'Old name' }],
'2026-10-01T00:00:00.000Z'
);
observeModerationPlayers(
handle,
't6',
[{ guid: '123', name: 'New name' }],
'2026-10-02T00:00:00.000Z'
);
observeModerationPlayers(
handle,
't6',
[{ guid: '123', name: 'Delayed name' }],
'2026-09-30T00:00:00.000Z'
);
handle.prepare('DELETE FROM leaderboard_players').run();
expect(handle.prepare('SELECT * FROM moderation_players').get()).toMatchObject({
game: 't6',
guid: '123',
name: 'New name',
first_seen_at: '2026-09-30T00:00:00.000Z',
last_seen_at: '2026-10-02T00:00:00.000Z'
});
expect(handle.prepare('SELECT name FROM moderation_aliases ORDER BY name').all()).toEqual([
{ name: 'Delayed name' },
{ name: 'New name' },
{ name: 'Old name' }
]);
});
it('records mutation results atomically and rejects reused keys or revoked access', async () => {
const { db } = await import('../../src/lib/server/db/index.js');
const { moderationMutation, moderationEvent } =
await import('../../src/lib/server/moderation/core.js');
const handle = db();
handle
.prepare(
`INSERT INTO auth_users(id,username,display_name,password_hash,role,created_at,updated_at)
VALUES('moderator','moderator','Moderator','unused','moderator',0,0)`
)
.run();
const body = { op: 'note', idempotencyKey: 'request-1', text: 'A note' };
const work = vi.fn((actor, now) => {
moderationEvent(actor, 'note', { game: 't5', guid: '123' }, null, 'A note', now);
return { id: 'one' };
});
expect(moderationMutation('moderator', body, work)).toEqual({ ok: true, id: 'one' });
expect(moderationMutation('moderator', body, work)).toEqual({ ok: true, id: 'one' });
expect(work).toHaveBeenCalledTimes(1);
expect(() => moderationMutation('moderator', { ...body, text: 'Changed' }, work)).toThrow(
'already used'
);
expect(() =>
moderationMutation('moderator', { ...body, idempotencyKey: 'failing' }, (actor, now) => {
moderationEvent(actor, 'note', {}, null, 'Rolled back', now);
throw new Error('Failed');
})
).toThrow('Failed');
expect(handle.prepare('SELECT COUNT(*) AS count FROM moderation_events').get()).toEqual({
count: 1
});
handle.prepare("UPDATE auth_users SET role='viewer' WHERE id='moderator'").run();
expect(() => moderationMutation('moderator', body, work)).toThrow('Moderation access');
});