This commit is contained in:
@@ -0,0 +1,145 @@
|
||||
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');
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user