146 lines
6.9 KiB
TypeScript
146 lines
6.9 KiB
TypeScript
import { randomUUID } from 'node:crypto';
|
|
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
|
import { newFixture, type Fixture } from '../helpers/fixture.js';
|
|
|
|
let fixture: Fixture;
|
|
let actorId: string;
|
|
let hostId: string;
|
|
let instanceId: string;
|
|
const server = 't5_zombie_theater_1';
|
|
beforeEach(async () => {
|
|
vi.resetModules();
|
|
vi.stubEnv('EC_PUBLIC_URL', 'https://public-controller.example');
|
|
fixture = await newFixture([]);
|
|
const hosts = await import('../../src/lib/server/hosts/index.js');
|
|
const { db } = await import('../../src/lib/server/db/index.js');
|
|
hostId = hosts.createHost({ name: 'Moderation host' }).id;
|
|
hosts.enrollHost(hosts.createEnrollment(hostId).token, {
|
|
capabilities: ['moderation.configure']
|
|
});
|
|
actorId = randomUUID();
|
|
instanceId = randomUUID();
|
|
db()
|
|
.prepare(
|
|
`INSERT INTO auth_users(id,username,display_name,password_hash,role,created_at,updated_at) VALUES(?,'admin','Admin','unused','admin',1,1)`
|
|
)
|
|
.run(actorId);
|
|
db()
|
|
.prepare(
|
|
`INSERT INTO native_instances(id,server,host_id,generation,library_id,image,port,created_at) VALUES(?,?,?,1,'library','image',28960,?)`
|
|
)
|
|
.run(instanceId, server, hostId, new Date().toISOString());
|
|
db().prepare(`INSERT INTO servers(name,game) VALUES(?,'t5')`).run(server);
|
|
});
|
|
afterEach(async () => {
|
|
await fixture.cleanup();
|
|
vi.unstubAllEnvs();
|
|
});
|
|
|
|
describe('moderation runtime configuration', () => {
|
|
it('protects the fixed private file even with customized shared config destinations', async () => {
|
|
const { config } = await import('../../src/lib/server/config.js');
|
|
(config as { relDestConfig: string }).relDestConfig = 'custom-configs';
|
|
const { isPrivateConfigPath } =
|
|
await import('../../src/lib/server/fleet/private-config.js');
|
|
expect(isPrivateConfigPath('configs/electric_cherry_moderation.cfg')).toBe(true);
|
|
expect(isPrivateConfigPath('custom-configs/electric_cherry_moderation.cfg')).toBe(true);
|
|
expect(isPrivateConfigPath('custom-configs/nested/electric_cherry_moderation.cfg')).toBe(
|
|
false
|
|
);
|
|
});
|
|
it('queues private configuration and returns only an operation identifier', async () => {
|
|
const { configureModerationServer } =
|
|
await import('../../src/lib/server/moderation/configure.js');
|
|
const hosts = await import('../../src/lib/server/hosts/index.js');
|
|
const { moderationCredential } =
|
|
await import('../../src/lib/server/moderation/credentials.js');
|
|
const result = configureModerationServer(actorId, server);
|
|
const credential = moderationCredential(instanceId, 1);
|
|
expect(Object.keys(result)).toEqual(['id']);
|
|
const operation = hosts.getHostOperation(result.id)!;
|
|
expect(operation).toMatchObject({
|
|
kind: 'instance.configure-moderation',
|
|
instanceId,
|
|
generation: 1,
|
|
payload: {
|
|
moderation: {
|
|
token: credential,
|
|
configPath: 'configs/electric_cherry_moderation.cfg',
|
|
url: 'https://public-controller.example/api/moderation/runtime',
|
|
generation: 1
|
|
}
|
|
}
|
|
});
|
|
expect(JSON.stringify(result)).not.toContain(credential);
|
|
expect(JSON.stringify(hosts.listHostOperationHistory())).not.toContain(credential);
|
|
const { db } = await import('../../src/lib/server/db/index.js');
|
|
expect(JSON.stringify(db().prepare('SELECT * FROM moderation_events').all())).not.toContain(
|
|
credential
|
|
);
|
|
expect(
|
|
db()
|
|
.prepare('SELECT count(*) AS n FROM file_blobs WHERE instr(content,?)>0')
|
|
.get(Buffer.from(credential))
|
|
).toEqual({ n: 0 });
|
|
hosts.reportHostOperationStatus(hostId, result.id, {
|
|
state: 'completed',
|
|
result: {
|
|
configured: true,
|
|
generation: 1,
|
|
liveLoad: 'sent',
|
|
text: credential,
|
|
contentBase64: Buffer.from(credential).toString('base64')
|
|
},
|
|
error: `Unexpected echo ${credential}`
|
|
});
|
|
const receipt = hosts.getHostOperationStatus(result.id)!;
|
|
expect(JSON.stringify(receipt)).not.toContain(credential);
|
|
expect(receipt.result).toEqual({ configured: true, generation: 1, liveLoad: 'sent' });
|
|
});
|
|
it.each(['viewer', 'operator', 'moderator', 'super_viewer'])(
|
|
'rejects %s configuration requests',
|
|
async (role) => {
|
|
const { db } = await import('../../src/lib/server/db/index.js');
|
|
db().prepare('UPDATE auth_users SET role=? WHERE id=?').run(role, actorId);
|
|
const { configureModerationServer } =
|
|
await import('../../src/lib/server/moderation/configure.js');
|
|
expect(() => configureModerationServer(actorId, server)).toThrow('access');
|
|
expect(db().prepare('SELECT COUNT(*) AS n FROM ec_host_operations').get()).toEqual({
|
|
n: 0
|
|
});
|
|
}
|
|
);
|
|
it('revalidates account state, host capability and instance identity before queueing', async () => {
|
|
const { configureModerationServer } =
|
|
await import('../../src/lib/server/moderation/configure.js');
|
|
const { db } = await import('../../src/lib/server/db/index.js');
|
|
db().prepare('UPDATE auth_users SET disabled=1 WHERE id=?').run(actorId);
|
|
expect(() => configureModerationServer(actorId, server)).toThrow('access');
|
|
db().prepare('UPDATE auth_users SET disabled=0 WHERE id=?').run(actorId);
|
|
const hosts = await import('../../src/lib/server/hosts/index.js');
|
|
hosts.heartbeat(hostId, { capabilities: [] });
|
|
expect(() => configureModerationServer(actorId, server)).toThrow('Update the host agent');
|
|
hosts.heartbeat(hostId, { capabilities: ['moderation.configure'] });
|
|
db()
|
|
.prepare('UPDATE native_instances SET retired_at=? WHERE id=?')
|
|
.run(new Date().toISOString(), instanceId);
|
|
expect(() => configureModerationServer(actorId, server)).toThrow('active native server');
|
|
expect(db().prepare('SELECT COUNT(*) AS n FROM ec_host_operations').get()).toEqual({
|
|
n: 0
|
|
});
|
|
});
|
|
it.each([
|
|
'',
|
|
'http://public-controller.example',
|
|
'https://user:password@public-controller.example',
|
|
'https://public-controller.example/prefix',
|
|
'https://public-controller.example/?token=secret'
|
|
])('rejects unsafe controller origin %s', async (url) => {
|
|
const { config } = await import('../../src/lib/server/config.js');
|
|
(config as { publicUrl: string }).publicUrl = url;
|
|
const { moderationRuntimeUrl } =
|
|
await import('../../src/lib/server/moderation/configuration.js');
|
|
expect(() => moderationRuntimeUrl()).toThrow('EC_PUBLIC_URL');
|
|
});
|
|
});
|