import { describe, it } from 'node:test'; import assert from 'node:assert/strict'; import { readFileSync } from 'node:fs'; import { join } from 'node:path'; import { parsePropertiesFile, serializePropertiesFile, } from '../dist/servers/properties/server-properties.util.js'; import { assertSafeServerPath } from '../dist/servers/shared/server-path.util.js'; describe('phase 3 server utilities', () => { it('parses server.properties with types', () => { const raw = [ '# comment', 'max-players=20', 'online-mode=false', 'motd=Hello World', '', ].join('\n'); const parsed = parsePropertiesFile(raw); assert.equal(parsed['max-players'], 20); assert.equal(parsed['online-mode'], false); assert.equal(parsed['motd'], 'Hello World'); }); it('serializes server.properties while preserving comments', () => { const raw = '# header\nmax-players=10\nonline-mode=true\n'; const updated = parsePropertiesFile(raw); updated['max-players'] = 25; updated['online-mode'] = false; const serialized = serializePropertiesFile(updated, raw); assert.match(serialized, /^# header/); assert.match(serialized, /max-players=25/); assert.match(serialized, /online-mode=false/); }); it('rejects path traversal', () => { assert.throws( () => assertSafeServerPath('../etc/passwd'), (error) => error.message.includes('traversal'), ); assert.throws(() => assertSafeServerPath('foo//bar')); }); it('normalizes safe paths', () => { assert.equal(assertSafeServerPath(''), '.'); assert.equal(assertSafeServerPath('logs/latest.log'), 'logs/latest.log'); assert.equal(assertSafeServerPath('\\world\\level.dat'), 'world/level.dat'); }); }); describe('phase 3 api contract', () => { it('documents console, files, properties and players routes', () => { const files = [ 'servers/console/console.controller.ts', 'servers/files/files.controller.ts', 'servers/properties/properties.controller.ts', 'servers/players/players.controller.ts', 'servers/console/console.gateway.ts', ]; for (const file of files) { const source = readFileSync(join(process.cwd(), 'src', file), 'utf8'); assert.ok(source.length > 0, `${file} should exist`); } const consoleSource = readFileSync( join(process.cwd(), 'src', 'servers/console/console.controller.ts'), 'utf8', ); assert.ok(consoleSource.includes("@Get('token')")); const gatewaySource = readFileSync( join(process.cwd(), 'src', 'servers/console/console.gateway.ts'), 'utf8', ); assert.ok(gatewaySource.includes('console/ws')); const filesSource = readFileSync( join(process.cwd(), 'src', 'servers/files/files.controller.ts'), 'utf8', ); assert.ok(filesSource.includes("@Get('content')")); const propertiesSource = readFileSync( join(process.cwd(), 'src', 'servers/properties/properties.controller.ts'), 'utf8', ); assert.ok(propertiesSource.includes("@Patch()")); const playersSource = readFileSync( join(process.cwd(), 'src', 'servers/players/players.controller.ts'), 'utf8', ); assert.ok(playersSource.includes('whitelist')); const combined = [ consoleSource, gatewaySource, filesSource, propertiesSource, playersSource, ].join('\n'); for (const snippet of ['files', 'properties', 'players']) { assert.ok(combined.includes(snippet), `missing route hint ${snippet}`); } }); });