This commit is contained in:
@@ -0,0 +1,590 @@
|
||||
import { randomUUID } from 'node:crypto';
|
||||
import { afterEach, beforeEach, expect, it, vi } from 'vitest';
|
||||
import { newFixture, type Fixture } from '../helpers/fixture.js';
|
||||
import type { ModerationRuntimeIdentity } from '../../src/lib/server/moderation/credentials.js';
|
||||
|
||||
const t5 = 't5_zombie_theater_1',
|
||||
t6 = 't6_zm_transit_1';
|
||||
let fixture: Fixture;
|
||||
let instanceId: string;
|
||||
let credential: string;
|
||||
let identity: ModerationRuntimeIdentity;
|
||||
const clock = '2026-10-15T12:00:00.000Z';
|
||||
|
||||
beforeEach(async () => {
|
||||
vi.resetModules();
|
||||
vi.useFakeTimers({ toFake: ['Date'] });
|
||||
vi.setSystemTime(new Date(clock));
|
||||
fixture = await newFixture([t5, t6]);
|
||||
const { seedServerRegistry } = await import('../../src/lib/server/fleet/servers.js');
|
||||
await seedServerRegistry();
|
||||
const { db } = await import('../../src/lib/server/db/index.js');
|
||||
const handle = db();
|
||||
for (const role of ['admin', 'moderator', 'viewer', 'operator', 'super_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);
|
||||
const { createHost } = await import('../../src/lib/server/hosts/index.js');
|
||||
const host = createHost({ name: 'Moderation host' });
|
||||
instanceId = randomUUID();
|
||||
handle
|
||||
.prepare(
|
||||
`INSERT INTO native_instances(id,server,host_id,library_id,image,port,created_at) VALUES(?,?,?,'library','image',28960,?)`
|
||||
)
|
||||
.run(instanceId, t5, host.id, clock);
|
||||
handle.prepare('UPDATE servers SET game=? WHERE name=?').run('t5', t5);
|
||||
handle.prepare('UPDATE servers SET game=? WHERE name=?').run('t6', t6);
|
||||
const { moderationCredential, authenticateModerationRuntime } =
|
||||
await import('../../src/lib/server/moderation/credentials.js');
|
||||
credential = moderationCredential(instanceId, 1);
|
||||
identity = authenticateModerationRuntime(
|
||||
new Request('https://test/api/moderation/runtime', {
|
||||
headers: { Authorization: `Bearer ${credential}` }
|
||||
})
|
||||
)!;
|
||||
expect(identity).toMatchObject({ instanceId, game: 't5', generation: 1 });
|
||||
});
|
||||
afterEach(async () => {
|
||||
vi.useRealTimers();
|
||||
await fixture.cleanup();
|
||||
});
|
||||
|
||||
async function mutate(input: Record<string, unknown>, actor = 'moderator') {
|
||||
const { mutateModeration } = await import('../../src/lib/server/moderation/actions.js');
|
||||
return mutateModeration(actor, { idempotencyKey: randomUUID(), ...input });
|
||||
}
|
||||
const restriction = (overrides: Record<string, unknown> = {}) => ({
|
||||
op: 'create-sanction',
|
||||
game: 't5',
|
||||
guid: '123',
|
||||
kind: 'ban',
|
||||
scope: 'game',
|
||||
reason: 'Repeated abuse',
|
||||
durationMinutes: 60,
|
||||
...overrides
|
||||
});
|
||||
async function sync(overrides: Record<string, unknown> = {}) {
|
||||
const { syncModerationRuntime } = await import('../../src/lib/server/moderation/runtime.js');
|
||||
return syncModerationRuntime(identity, {
|
||||
protocol: 1,
|
||||
server: t5,
|
||||
generation: 1,
|
||||
session: 'boot-1',
|
||||
sessionSequence: 1,
|
||||
players: [{ guid: '123', name: 'Player', connection: 'boot-1:1' }],
|
||||
capabilities: ['warn', 'message', 'kick', 'announce', 'ban', 'mute'],
|
||||
appliedRevision: 0,
|
||||
receipts: [],
|
||||
...overrides
|
||||
});
|
||||
}
|
||||
|
||||
it('enforces private read/write access, duration limits, and explicit fleet identity', async () => {
|
||||
const { moderationOverview, moderationQuery } =
|
||||
await import('../../src/lib/server/moderation/overview.js');
|
||||
const query = moderationQuery(new URLSearchParams());
|
||||
for (const role of ['viewer', 'operator'])
|
||||
expect(() => moderationOverview(role, query)).toThrow('Moderation access');
|
||||
expect(moderationOverview('super_viewer', query).total).toBe(0);
|
||||
for (const role of ['viewer', 'operator', 'super_viewer'])
|
||||
await expect(mutate(restriction(), role)).rejects.toThrow('Moderation access');
|
||||
for (const input of [{ durationMinutes: null }, { durationMinutes: 10081 }, { scope: 'fleet' }])
|
||||
await expect(mutate(restriction(input))).rejects.toThrow('up to 7 days');
|
||||
await expect(mutate(restriction({ scope: 'fleet' }), 'admin')).rejects.toThrow('both games');
|
||||
expect(await mutate(restriction({ durationMinutes: 10080 }))).toHaveProperty('id');
|
||||
});
|
||||
|
||||
it('applies current moderator limits when revoking restrictions created before an administrator downgrade', async () => {
|
||||
const { db } = await import('../../src/lib/server/db/index.js');
|
||||
const { observeModerationPlayers } = await import('../../src/lib/server/moderation/core.js');
|
||||
for (const game of ['t5', 't6'] as const)
|
||||
observeModerationPlayers(db(), game, [{ guid: '123', name: 'Verified player' }], clock);
|
||||
db().prepare("UPDATE auth_users SET role='admin' WHERE id='moderator'").run();
|
||||
const restricted = await Promise.all(
|
||||
[
|
||||
{ scope: 'fleet', durationMinutes: 10080 },
|
||||
{ durationMinutes: null },
|
||||
{ durationMinutes: 11520 }
|
||||
].map((input) => mutate(restriction(input)))
|
||||
);
|
||||
const allowed = await Promise.all(
|
||||
[{ scope: 'server', server: t5 }, { scope: 'game' }].map((input) =>
|
||||
mutate(restriction({ ...input, durationMinutes: 10080 }))
|
||||
)
|
||||
);
|
||||
db().prepare("UPDATE auth_users SET role='moderator' WHERE id='moderator'").run();
|
||||
// The eight-day restriction now has only six days left, but its original duration governs.
|
||||
vi.setSystemTime(new Date(Date.parse(clock) + 2 * 24 * 60 * 60 * 1000));
|
||||
const revocation = (id: string) => ({
|
||||
op: 'revoke-sanction',
|
||||
id,
|
||||
revision: 1,
|
||||
reason: 'Appeal accepted'
|
||||
});
|
||||
for (const { id } of restricted)
|
||||
await expect(mutate(revocation(id))).rejects.toThrow('originally lasting up to 7 days');
|
||||
expect(
|
||||
db()
|
||||
.prepare(
|
||||
'SELECT COUNT(*) AS count FROM moderation_sanctions WHERE revoked_at IS NOT NULL'
|
||||
)
|
||||
.get()
|
||||
).toEqual({ count: 0 });
|
||||
for (const { id } of allowed) expect(await mutate(revocation(id))).toMatchObject({ id });
|
||||
for (const { id } of restricted)
|
||||
expect(await mutate(revocation(id), 'admin')).toMatchObject({ id });
|
||||
});
|
||||
|
||||
it('applies game and exact server registration scopes and revisioned revocation', async () => {
|
||||
const game = await mutate(restriction());
|
||||
const server = await mutate(restriction({ scope: 'server', server: t5, kind: 'mute' }));
|
||||
const initial = await sync();
|
||||
expect(initial.sanctions).toHaveLength(2);
|
||||
await expect(
|
||||
mutate({ op: 'revoke-sanction', id: game.id, revision: 99, reason: 'Correction' })
|
||||
).rejects.toThrow('changed');
|
||||
const { db } = await import('../../src/lib/server/db/index.js');
|
||||
db()
|
||||
.prepare(
|
||||
"INSERT INTO auth_users(id,username,display_name,password_hash,role,created_at,updated_at) VALUES('other','other','Other','unused','moderator',0,0)"
|
||||
)
|
||||
.run();
|
||||
await expect(
|
||||
mutate({ op: 'revoke-sanction', id: game.id, revision: 1, reason: 'Correction' }, 'other')
|
||||
).rejects.toThrow('another moderator');
|
||||
await mutate(
|
||||
{ op: 'revoke-sanction', id: game.id, revision: 1, reason: 'Appeal accepted' },
|
||||
'admin'
|
||||
);
|
||||
const next = await sync({ appliedRevision: initial.revision });
|
||||
expect(next.revision).toBeGreaterThan(initial.revision);
|
||||
expect(next.sanctions).toMatchObject([{ id: server.id }]);
|
||||
const { syncModerationRuntime } = await import('../../src/lib/server/moderation/runtime.js');
|
||||
const differentRegistration = { ...identity, serverCreatedAt: '2099-01-01T00:00:00.000Z' };
|
||||
db()
|
||||
.prepare('UPDATE servers SET created_at=? WHERE name=?')
|
||||
.run(differentRegistration.serverCreatedAt, t5);
|
||||
expect(
|
||||
syncModerationRuntime(differentRegistration, {
|
||||
protocol: 1,
|
||||
server: t5,
|
||||
generation: 1,
|
||||
session: 'boot-1',
|
||||
sessionSequence: 1,
|
||||
players: [],
|
||||
capabilities: [],
|
||||
appliedRevision: 0,
|
||||
receipts: []
|
||||
}).sanctions
|
||||
).toEqual([]);
|
||||
});
|
||||
|
||||
it('excludes expired restrictions without deleting their history', async () => {
|
||||
await mutate(restriction({ durationMinutes: 1 }));
|
||||
expect((await sync()).sanctions).toHaveLength(1);
|
||||
vi.setSystemTime(new Date(Date.parse(clock) + 60_000));
|
||||
expect((await sync()).sanctions).toEqual([]);
|
||||
const { moderationOverview, moderationQuery } =
|
||||
await import('../../src/lib/server/moderation/overview.js');
|
||||
const data = moderationOverview(
|
||||
'moderator',
|
||||
moderationQuery(new URLSearchParams('view=restrictions'))
|
||||
);
|
||||
expect(data.sanctions).toMatchObject([{ active: false, reason: 'Repeated abuse' }]);
|
||||
});
|
||||
|
||||
it('queues a single live action and preserves exact delivery across duplicate polling', async () => {
|
||||
await sync();
|
||||
const request = {
|
||||
op: 'action',
|
||||
kind: 'kick',
|
||||
game: 't5',
|
||||
guid: '123',
|
||||
server: t5,
|
||||
reason: 'Repeated abuse',
|
||||
idempotencyKey: 'same-action'
|
||||
};
|
||||
const created = await mutate(request);
|
||||
expect(await mutate(request)).toEqual(created);
|
||||
const first = await sync();
|
||||
expect(first.actions).toMatchObject([
|
||||
{ id: created.id, guid: '123', connection: 'boot-1:1', kind: 'kick' }
|
||||
]);
|
||||
expect((await sync()).actions).toEqual(first.actions);
|
||||
expect(
|
||||
(
|
||||
await sync({
|
||||
receipts: [{ id: created.id, state: 'confirmed', detail: 'Player disconnected.' }]
|
||||
})
|
||||
).actions
|
||||
).toEqual([]);
|
||||
await sync({
|
||||
receipts: [{ id: created.id, state: 'confirmed', detail: 'Player disconnected.' }]
|
||||
});
|
||||
const { db } = await import('../../src/lib/server/db/index.js');
|
||||
expect(
|
||||
db()
|
||||
.prepare(
|
||||
"SELECT COUNT(*) AS count FROM moderation_events WHERE action='action.confirmed'"
|
||||
)
|
||||
.get()
|
||||
).toEqual({ count: 1 });
|
||||
});
|
||||
|
||||
it('does not deliver to a reused connection or after permission revocation', async () => {
|
||||
await sync();
|
||||
await mutate({
|
||||
op: 'action',
|
||||
kind: 'warn',
|
||||
game: 't5',
|
||||
guid: '123',
|
||||
server: t5,
|
||||
reason: 'Warning'
|
||||
});
|
||||
expect(
|
||||
(await sync({ players: [{ guid: '123', name: 'Player', connection: 'boot-1:2' }] })).actions
|
||||
).toEqual([]);
|
||||
await mutate({
|
||||
op: 'action',
|
||||
kind: 'warn',
|
||||
game: 't5',
|
||||
guid: '123',
|
||||
server: t5,
|
||||
reason: 'Warning again'
|
||||
});
|
||||
const { db } = await import('../../src/lib/server/db/index.js');
|
||||
db().prepare("UPDATE auth_users SET role='viewer' WHERE id='moderator'").run();
|
||||
expect(
|
||||
(await sync({ players: [{ guid: '123', name: 'Player', connection: 'boot-1:2' }] })).actions
|
||||
).toEqual([]);
|
||||
expect(db().prepare('SELECT state FROM moderation_actions').all()).toEqual([
|
||||
{ state: 'failed' },
|
||||
{ state: 'failed' }
|
||||
]);
|
||||
});
|
||||
|
||||
it('keeps unacknowledged delivery uncertain and accepts durable late receipts without replay', async () => {
|
||||
await sync();
|
||||
const { id } = await mutate({
|
||||
op: 'action',
|
||||
kind: 'kick',
|
||||
game: 't5',
|
||||
guid: '123',
|
||||
server: t5,
|
||||
reason: 'Kick'
|
||||
});
|
||||
await sync();
|
||||
vi.setSystemTime(new Date(Date.parse(clock) + 31_000));
|
||||
expect((await sync()).actions).toEqual([]);
|
||||
const { db } = await import('../../src/lib/server/db/index.js');
|
||||
expect(db().prepare('SELECT state FROM moderation_actions WHERE id=?').get(id)).toEqual({
|
||||
state: 'unknown'
|
||||
});
|
||||
await sync({
|
||||
session: 'boot-2',
|
||||
sessionSequence: 2,
|
||||
players: [],
|
||||
receipts: [{ id, state: 'confirmed', detail: 'Observed disconnect' }]
|
||||
});
|
||||
expect(db().prepare('SELECT state FROM moderation_actions WHERE id=?').get(id)).toEqual({
|
||||
state: 'confirmed'
|
||||
});
|
||||
});
|
||||
|
||||
it('expires undelivered actions and refuses stale or unsupported live targeting', async () => {
|
||||
await sync({ capabilities: ['warn'] });
|
||||
await expect(
|
||||
mutate({ op: 'action', kind: 'kick', game: 't5', guid: '123', server: t5, reason: 'Kick' })
|
||||
).rejects.toThrow('does not support');
|
||||
await expect(
|
||||
mutate({
|
||||
op: 'action',
|
||||
kind: 'warn',
|
||||
game: 't5',
|
||||
guid: '999',
|
||||
server: t5,
|
||||
reason: 'Warning'
|
||||
})
|
||||
).rejects.toThrow('current connection');
|
||||
await mutate({
|
||||
op: 'action',
|
||||
kind: 'warn',
|
||||
game: 't5',
|
||||
guid: '123',
|
||||
server: t5,
|
||||
reason: 'Warning'
|
||||
});
|
||||
vi.setSystemTime(new Date(Date.parse(clock) + 31_000));
|
||||
await expect(
|
||||
mutate({
|
||||
op: 'action',
|
||||
kind: 'warn',
|
||||
game: 't5',
|
||||
guid: '123',
|
||||
server: t5,
|
||||
reason: 'Warning'
|
||||
})
|
||||
).rejects.toThrow('not connected');
|
||||
await sync();
|
||||
const { db } = await import('../../src/lib/server/db/index.js');
|
||||
expect(db().prepare('SELECT state FROM moderation_actions').get()).toEqual({
|
||||
state: 'expired'
|
||||
});
|
||||
});
|
||||
|
||||
it('binds machine credentials to the active generation and authenticates before body parsing', async () => {
|
||||
const { POST } = await import('../../src/routes/api/moderation/runtime/+server.js');
|
||||
const invoke = (token: string) =>
|
||||
POST({
|
||||
request: new Request('https://test/api/moderation/runtime', {
|
||||
method: 'POST',
|
||||
headers: { Authorization: `Bearer ${token}` },
|
||||
body: 'bad json'
|
||||
})
|
||||
} as Parameters<typeof POST>[0]);
|
||||
expect((await invoke('ec_bank_' + 'a'.repeat(43))).status).toBe(401);
|
||||
const { db } = await import('../../src/lib/server/db/index.js');
|
||||
const { moderationCredential, authenticateModerationRuntime } =
|
||||
await import('../../src/lib/server/moderation/credentials.js');
|
||||
const future = moderationCredential(instanceId, 2);
|
||||
expect(
|
||||
authenticateModerationRuntime(
|
||||
new Request('https://test', { headers: { Authorization: `Bearer ${future}` } })
|
||||
)
|
||||
).toBeNull();
|
||||
db().prepare('UPDATE native_instances SET generation=2 WHERE id=?').run(instanceId);
|
||||
expect((await invoke(credential)).status).toBe(401);
|
||||
expect(
|
||||
authenticateModerationRuntime(
|
||||
new Request('https://test', { headers: { Authorization: `Bearer ${future}` } })
|
||||
)
|
||||
).toMatchObject({ generation: 2 });
|
||||
db().prepare('UPDATE native_instances SET retired_at=? WHERE id=?').run(clock, instanceId);
|
||||
expect((await invoke(future)).status).toBe(401);
|
||||
});
|
||||
|
||||
it('preserves verified chat evidence after source match cleanup and rejects forged evidence', async () => {
|
||||
const { saveTelemetry } = await import('../../src/lib/server/fleet/telemetry.js');
|
||||
saveTelemetry({
|
||||
server: t5,
|
||||
map: 'zombie_theater',
|
||||
event: 'game_start',
|
||||
players: [{ guid: '123', name: 'Player' }],
|
||||
receivedAt: clock
|
||||
});
|
||||
const { db } = await import('../../src/lib/server/db/index.js');
|
||||
const { id: matchId } = db().prepare('SELECT id FROM leaderboard_matches').get() as {
|
||||
id: number;
|
||||
};
|
||||
const entry = {
|
||||
type: 'message',
|
||||
message: {
|
||||
guid: '123',
|
||||
slot: 0,
|
||||
player: 'Player',
|
||||
message: 'Evidence message',
|
||||
time: '1:20'
|
||||
},
|
||||
context: { realTime: null, round: null, matchElapsedMs: null }
|
||||
};
|
||||
db()
|
||||
.prepare('INSERT INTO match_chat VALUES(?,?,?,?)')
|
||||
.run(matchId, 'key', 0, JSON.stringify(entry));
|
||||
await expect(
|
||||
mutate({
|
||||
op: 'create-case',
|
||||
game: 't5',
|
||||
guid: '123',
|
||||
title: 'Review',
|
||||
evidence: { matchId, message: 'Forged message' }
|
||||
})
|
||||
).rejects.toThrow('could not be verified');
|
||||
const { id } = await mutate({
|
||||
op: 'create-case',
|
||||
game: 't5',
|
||||
guid: '123',
|
||||
title: 'Review',
|
||||
note: 'Staff context',
|
||||
evidence: { matchId, message: 'Evidence message' }
|
||||
});
|
||||
db().prepare('DELETE FROM leaderboard_matches').run();
|
||||
expect(
|
||||
db()
|
||||
.prepare(
|
||||
'SELECT text,source,snapshot FROM moderation_evidence WHERE case_id=? ORDER BY source'
|
||||
)
|
||||
.all(id)
|
||||
).toEqual([
|
||||
expect.objectContaining({
|
||||
text: 'Evidence message',
|
||||
snapshot: expect.stringContaining('Evidence message')
|
||||
}),
|
||||
expect.objectContaining({ text: 'Staff context', source: 'Staff note', snapshot: null })
|
||||
]);
|
||||
await mutate({
|
||||
op: 'resolve-case',
|
||||
id,
|
||||
revision: 1,
|
||||
status: 'resolved',
|
||||
reason: 'Appeal accepted'
|
||||
});
|
||||
await expect(
|
||||
mutate({
|
||||
op: 'resolve-case',
|
||||
id,
|
||||
revision: 1,
|
||||
status: 'dismissed',
|
||||
reason: 'Stale decision'
|
||||
})
|
||||
).rejects.toThrow('changed');
|
||||
});
|
||||
|
||||
it('retains paginated player search and reads pending policy coverage honestly', async () => {
|
||||
await sync();
|
||||
await mutate(restriction());
|
||||
const { moderationOverview, moderationQuery } =
|
||||
await import('../../src/lib/server/moderation/overview.js');
|
||||
const data = moderationOverview(
|
||||
'moderator',
|
||||
moderationQuery(new URLSearchParams('q=Player&offset=999'))
|
||||
);
|
||||
expect(data.players).toMatchObject([
|
||||
{ guid: '123', name: 'Player', servers: [t5], restrictions: 1 }
|
||||
]);
|
||||
expect(data.query.offset).toBe(0);
|
||||
expect(data.coverage.find((c) => c.server === t5)?.state).toBe('pending');
|
||||
const snapshot = await sync();
|
||||
await sync({ appliedRevision: snapshot.revision });
|
||||
expect(
|
||||
moderationOverview('moderator', data.query).coverage.find((c) => c.server === t5)?.state
|
||||
).toBe('current');
|
||||
});
|
||||
|
||||
it('does not treat manually entered identities as cross-game observations', async () => {
|
||||
for (const game of ['t5', 't6'])
|
||||
await mutate({ op: 'note', game, guid: '123', text: 'Manual lookup' });
|
||||
await expect(mutate(restriction({ scope: 'fleet' }), 'admin')).rejects.toThrow('both games');
|
||||
const { moderationOverview, moderationQuery } =
|
||||
await import('../../src/lib/server/moderation/overview.js');
|
||||
expect(moderationOverview('admin', moderationQuery(new URLSearchParams())).players).toEqual([
|
||||
expect.objectContaining({ firstSeen: null, lastSeen: null }),
|
||||
expect.objectContaining({ firstSeen: null, lastSeen: null })
|
||||
]);
|
||||
await sync();
|
||||
const { db } = await import('../../src/lib/server/db/index.js');
|
||||
const { observeModerationPlayers } = await import('../../src/lib/server/moderation/core.js');
|
||||
observeModerationPlayers(db(), 't6', [{ guid: '123', name: 'Verified player' }], clock);
|
||||
const created = await mutate(restriction({ scope: 'fleet' }), 'admin');
|
||||
expect((await sync()).sanctions).toMatchObject([{ id: created.id }]);
|
||||
});
|
||||
|
||||
it('shows applicable fleet restrictions in either game without including other game restrictions', async () => {
|
||||
const { db } = await import('../../src/lib/server/db/index.js');
|
||||
const { observeModerationPlayers } = await import('../../src/lib/server/moderation/core.js');
|
||||
for (const game of ['t5', 't6'] as const)
|
||||
observeModerationPlayers(db(), game, [{ guid: '123', name: 'Verified player' }], clock);
|
||||
const fleet = await mutate(restriction({ scope: 'fleet' }), 'admin');
|
||||
const local = {
|
||||
t5: await mutate(restriction()),
|
||||
t6: await mutate(restriction({ game: 't6' }))
|
||||
};
|
||||
await mutate(restriction({ game: 't6', guid: '456' }));
|
||||
const { moderationOverview, moderationQuery } =
|
||||
await import('../../src/lib/server/moderation/overview.js');
|
||||
for (const game of ['t5', 't6'] as const) {
|
||||
const data = moderationOverview(
|
||||
'moderator',
|
||||
moderationQuery(new URLSearchParams({ view: 'restrictions', game, guid: '123' }))
|
||||
);
|
||||
expect(data.total).toBe(2);
|
||||
expect(data.sanctions.map((row) => row.id).sort()).toEqual(
|
||||
[fleet.id, local[game].id].sort()
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
it('rejects oversized UTF-8 game messages before policy or queue changes', async () => {
|
||||
await expect(mutate(restriction({ reason: '界'.repeat(100) }))).rejects.toThrow('UTF-8 bytes');
|
||||
await sync();
|
||||
await expect(
|
||||
mutate({
|
||||
op: 'action',
|
||||
kind: 'message',
|
||||
game: 't5',
|
||||
guid: '123',
|
||||
server: t5,
|
||||
reason: '界'.repeat(100)
|
||||
})
|
||||
).rejects.toThrow('UTF-8 bytes');
|
||||
const { db } = await import('../../src/lib/server/db/index.js');
|
||||
expect(db().prepare('SELECT COUNT(*) AS count FROM moderation_sanctions').get()).toEqual({
|
||||
count: 0
|
||||
});
|
||||
expect(db().prepare('SELECT COUNT(*) AS count FROM moderation_actions').get()).toEqual({
|
||||
count: 0
|
||||
});
|
||||
});
|
||||
|
||||
it('rechecks runtime authorization after a request body could have yielded', async () => {
|
||||
const { db } = await import('../../src/lib/server/db/index.js');
|
||||
db().prepare('UPDATE native_instances SET generation=2 WHERE id=?').run(instanceId);
|
||||
await expect(sync()).rejects.toThrow('no longer active');
|
||||
expect(db().prepare('SELECT COUNT(*) AS count FROM moderation_runtimes').get()).toEqual({
|
||||
count: 0
|
||||
});
|
||||
db().prepare('UPDATE native_instances SET generation=1 WHERE id=?').run(instanceId);
|
||||
db().prepare('DELETE FROM moderation_credentials WHERE instance_id=?').run(instanceId);
|
||||
await expect(sync()).rejects.toThrow('no longer active');
|
||||
expect(db().prepare('SELECT COUNT(*) AS count FROM moderation_players').get()).toEqual({
|
||||
count: 0
|
||||
});
|
||||
});
|
||||
|
||||
it('rejects coerced enum values before moderator scope checks or SQL binding', async () => {
|
||||
await expect(mutate(restriction({ scope: ['fleet'] }))).rejects.toThrow('restriction scope');
|
||||
await sync();
|
||||
await expect(
|
||||
mutate({ op: 'action', kind: ['announce'], server: t5, reason: 'Message' })
|
||||
).rejects.toThrow('supported live action');
|
||||
const { db } = await import('../../src/lib/server/db/index.js');
|
||||
expect(db().prepare('SELECT COUNT(*) AS count FROM moderation_sanctions').get()).toEqual({
|
||||
count: 0
|
||||
});
|
||||
expect(db().prepare('SELECT COUNT(*) AS count FROM moderation_requests').get()).toEqual({
|
||||
count: 0
|
||||
});
|
||||
});
|
||||
|
||||
it('rejects delayed polls from superseded game sessions without replacing current connections', async () => {
|
||||
await sync({
|
||||
session: 'new-boot',
|
||||
sessionSequence: 2,
|
||||
players: [{ guid: '123', name: 'Current player', connection: 'new-boot:1' }]
|
||||
});
|
||||
await expect(sync()).rejects.toThrow('superseded');
|
||||
await expect(sync({ session: 'different-boot', sessionSequence: 2 })).rejects.toThrow(
|
||||
'superseded'
|
||||
);
|
||||
const { db } = await import('../../src/lib/server/db/index.js');
|
||||
expect(
|
||||
db().prepare('SELECT session,session_sequence,players FROM moderation_runtimes').get()
|
||||
).toMatchObject({
|
||||
session: 'new-boot',
|
||||
session_sequence: 2,
|
||||
players: expect.stringContaining('Current player')
|
||||
});
|
||||
});
|
||||
|
||||
it('does not advertise current restriction coverage for a runtime lacking ban or mute support', async () => {
|
||||
const first = await sync();
|
||||
await sync({ capabilities: ['warn'], appliedRevision: first.revision });
|
||||
const { moderationOverview, moderationQuery } =
|
||||
await import('../../src/lib/server/moderation/overview.js');
|
||||
expect(
|
||||
moderationOverview('admin', moderationQuery(new URLSearchParams())).coverage.find(
|
||||
(row) => row.server === t5
|
||||
)
|
||||
).toMatchObject({ state: 'pending', capabilities: ['warn'] });
|
||||
});
|
||||
Reference in New Issue
Block a user