import test from 'node:test'; import assert from 'node:assert/strict'; import fs from 'node:fs'; import * as cryptoModule from 'node:crypto'; import path from 'node:path'; import { verifyRelease, readSettings, runtime } from '../platform/runtime.mjs'; import { fixture, seal } from './helpers/runtime-fixture.mjs'; test('release validation rejects tampering, incomplete coverage, and extra files', t => { const f = fixture(t); assert(verifyRelease(f.release).identity); fs.appendFileSync(path.join(f.release, 'fixture.mrpack'), 'tampered'); assert.throws(() => verifyRelease(f.release), /checksum mismatch/); seal(f.release); fs.writeFileSync(path.join(f.release, 'extra.txt'), 'unexpected'); assert.throws(() => verifyRelease(f.release), /coverage/); }); test('EULA/identity must be explicit and startup refusal leaves data/binding absent', t => { const f = fixture(t), settings = path.join(f.host, '.env'); fs.writeFileSync(settings, fs.readFileSync(settings, 'utf8').replace('MC_EULA=TRUE', 'MC_EULA=FALSE')); assert.throws(() => f.run(['up', '-d']), /EULA acceptance/); assert(!fs.existsSync(path.join(f.host, 'runtime'))); assert(!fs.existsSync(path.join(f.host, '.mcpack-runtime.json'))); fs.appendFileSync(settings, 'MC_UID=123\n'); assert.throws(() => readSettings(settings), /Invalid local settings/); }); test('fresh up binds identity, preserves local UID over ambient env, and same-release recreate works', t => { const f = fixture(t); f.env.MC_UID = '999'; assert.equal(f.run(['up', '-d']), 0); const state = JSON.parse(fs.readFileSync(path.join(f.host, '.mcpack-runtime.json'))); assert.equal(state.project, 'fixture-server'); assert.equal(state.release_manifest_sha256, verifyRelease(f.release).identity); const invocation = JSON.parse(fs.readFileSync(path.join(f.root, '.tools/invocation.json'))); assert.equal(invocation.uid, '1000'); assert.equal(invocation.gid, '1000'); assert.deepEqual(invocation.args.slice(0, 2), ['--project-name', 'fixture-server']); fs.writeFileSync(path.join(f.host, 'runtime/world-probe'), 'preserve'); assert.equal(f.run(['create', '--force-recreate']), 0); assert.equal(fs.readFileSync(path.join(f.host, 'runtime/world-probe'), 'utf8'), 'preserve'); }); test('another release/project cannot be applied to a bound runtime', t => { const f = fixture(t); f.run(['up', '-d']); f.env.MC_PROJECT_NAME = 'another-server'; assert.throws(() => f.run(['up', '-d']), /bound to another/); f.env.MC_PROJECT_NAME = 'fixture-server'; fs.appendFileSync(path.join(f.release, 'fixture.mrpack'), 'another release'); seal(f.release); assert.throws(() => f.run(['up', '-d']), /bound to another/); }); test('existing unidentified world cannot be silently adopted', t => { const f = fixture(t); fs.mkdirSync(path.join(f.host, 'runtime')); fs.writeFileSync(path.join(f.host, 'runtime/level.dat'), 'valuable data'); assert.throws(() => f.run(['up', '-d']), /Nonempty runtime/); assert.equal(fs.readFileSync(path.join(f.host, 'runtime/level.dat'), 'utf8'), 'valuable data'); }); test('failed Compose startup preserves binding, propagates exit code, and releases lock', t => { const f = fixture(t); fs.writeFileSync(path.join(f.root, '.tools/fail-start'), ''); assert.equal(f.run(['up', '-d']), 19); assert(fs.existsSync(path.join(f.host, '.mcpack-runtime.json'))); assert(!fs.existsSync(path.join(f.host, '.mcpack-runtime.lock'))); }); test('operation lock and compiled offline contract are enforced before runtime mutation', t => { const f = fixture(t); fs.mkdirSync(path.join(f.host, '.mcpack-runtime.lock')); assert.throws(() => f.run(['up', '-d']), /Another runtime operation/); assert(!fs.existsSync(path.join(f.host, 'runtime'))); fs.rmdirSync(path.join(f.host, '.mcpack-runtime.lock')); fs.writeFileSync(path.join(f.root, '.tools/wrong-contract'), ''); assert.throws(() => f.run(['up', '-d']), /offline release contract/); assert(!fs.existsSync(path.join(f.host, '.mcpack-runtime.json'))); }); test('unsafe paths and Compose file/project overrides are rejected', t => { const f = fixture(t); assert.throws(() => f.run(['up', '-f', 'another.yaml']), /controlled by the wrapper/); assert.throws(() => f.run(['up', '-fanother.yaml']), /controlled by the wrapper/); f.env.MC_ENV_FILE = path.join(f.release, 'server/.env.example'); assert.throws(() => f.run(['up']), /outside the immutable release/); delete f.env.MC_ENV_FILE; f.env.MC_ROOT = 'relative'; assert.throws(() => f.run(['up']), /absolute path/); }); test('existing Compose project with another data root cannot be replaced or stopped', t => { const f = fixture(t); fs.writeFileSync(path.join(f.root, '.tools/existing-project'), ''); assert.throws(() => f.run(['up', '-d']), /belongs to another runtime root/); assert.throws(() => f.run(['down']), /belongs to another runtime root/); assert(!fs.existsSync(path.join(f.host, 'runtime'))); assert(!fs.existsSync(path.join(f.host, '.mcpack-runtime.json'))); assert(!fs.existsSync(path.join(f.host, '.mcpack-runtime.lock'))); }); test('bind address is validated and host settings override ambient bind address', t => { const f=fixture(t),settings=path.join(f.host,'.env'); assert.equal(readSettings(settings).MC_BIND_ADDRESS,'127.0.0.1'); fs.writeFileSync(settings,fs.readFileSync(settings,'utf8').replace('MC_BIND_ADDRESS=127.0.0.1','MC_BIND_ADDRESS=0.0.0.0')); assert.equal(readSettings(settings).MC_BIND_ADDRESS,'0.0.0.0'); f.env.MC_BIND_ADDRESS='127.0.0.1'; assert.equal(f.run(['up','-d']),0); assert.equal(JSON.parse(fs.readFileSync(path.join(f.root,'.tools/invocation.json'))).bind,'0.0.0.0'); fs.writeFileSync(settings,fs.readFileSync(settings,'utf8').replace('MC_BIND_ADDRESS=0.0.0.0','MC_BIND_ADDRESS=invalid.host')); assert.throws(()=>readSettings(settings),/IPv4 address/); }); test('legacy immutable Compose honors explicit host bind using a temporary port-only overlay', t => { const f=fixture(t),repository=new URL('../',import.meta.url).pathname; fs.copyFileSync(path.join(repository,'.tools/docker-compose'),path.join(f.root,'.tools/docker-compose')); const image=JSON.parse(fs.readFileSync(path.join(f.release,'manifest.json'))).server.image; const compose=`services:\n minecraft:\n image: ${image}\n environment:\n TYPE: MODRINTH\n ONLINE_MODE: "false"\n ENFORCE_SECURE_PROFILE: "false"\n MODRINTH_MODPACK: /modpacks/fixture.mrpack\n ports:\n - "127.0.0.1:25565:25565"\n volumes:\n - "\${MC_ROOT}/runtime:/data"\n - "\${MC_RELEASE_DIR}:/modpacks:ro"\n - "\${MC_RELEASE_DIR}/server/config:/config:ro"\n`; fs.writeFileSync(path.join(f.release,'server/compose.yaml'),compose); const manifest=JSON.parse(fs.readFileSync(path.join(f.release,'manifest.json'))); const {createHash}=cryptoModule; manifest.tools.compose.sha256=createHash('sha256').update(fs.readFileSync(path.join(f.root,'.tools/docker-compose'))).digest('hex'); fs.writeFileSync(path.join(f.release,'manifest.json'),JSON.stringify(manifest));seal(f.release); fs.writeFileSync(path.join(f.host,'.env'),fs.readFileSync(path.join(f.host,'.env'),'utf8').replace('MC_BIND_ADDRESS=127.0.0.1','MC_BIND_ADDRESS=0.0.0.0')); const before=fs.readFileSync(path.join(f.release,'server/compose.yaml'),'utf8'); const result=JSON.parse(runtime({repository:f.root,env:f.env,args:['config','--format','json'],capture:true})); assert.equal(result.services.minecraft.ports.length,1);assert.equal(result.services.minecraft.ports[0].host_ip,'0.0.0.0'); assert.equal(fs.readFileSync(path.join(f.release,'server/compose.yaml'),'utf8'),before); assert(!fs.existsSync(path.join(f.host,'runtime'))); });