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'); }); });