166 lines
8.7 KiB
JavaScript
166 lines
8.7 KiB
JavaScript
import test from 'node:test';
|
|
import assert from 'node:assert/strict';
|
|
import fs from 'node:fs';
|
|
import os from 'node:os';
|
|
import path from 'node:path';
|
|
import crypto from 'node:crypto';
|
|
import { execFileSync } from 'node:child_process';
|
|
import { fileURLToPath } from 'node:url';
|
|
import { parse, stringify } from 'smol-toml';
|
|
import { parse as parseYaml, stringify as stringifyYaml } from 'yaml';
|
|
import { build } from '../platform/build.mjs';
|
|
|
|
import {fixture,commit} from './helpers/build-fixture.mjs';
|
|
const repository=fileURLToPath(new URL('../',import.meta.url));
|
|
const sha=file=>crypto.createHash('sha256').update(fs.readFileSync(file)).digest('hex');
|
|
const git=(root,args)=>execFileSync('git',args,{cwd:root,stdio:'pipe'});
|
|
function refresh(root) {
|
|
const directory = path.join(root, 'pack');
|
|
const pack = parse(fs.readFileSync(path.join(directory, 'pack.toml'), 'utf8'));
|
|
const index = parse(fs.readFileSync(path.join(directory, pack.index.file), 'utf8'));
|
|
for (const entry of index.files) entry.hash = sha(path.join(directory, entry.file));
|
|
fs.writeFileSync(path.join(directory, pack.index.file), stringify(index));
|
|
pack.index.hash = sha(path.join(directory, pack.index.file));
|
|
fs.writeFileSync(path.join(directory, 'pack.toml'), stringify(pack));
|
|
}
|
|
|
|
const perform = (root, options = {}) => build({ root, log: () => {}, ...options });
|
|
const leftovers = root => fs.existsSync(path.join(root, 'dist')) ? fs.readdirSync(path.join(root, 'dist')).filter(file => file.startsWith('.build-')) : [];
|
|
|
|
test('full clean release has portable Compose, complete checksums, ownership and no secrets/target', t => {
|
|
const root = fixture(t);
|
|
const configFile = path.join(root, 'platform/config.yaml');
|
|
const config = parseYaml(fs.readFileSync(configFile, 'utf8'));
|
|
config.server.memory = '3G'; config.server.port = 25570;
|
|
config.server.host = 'private-deployment-host'; config.server.directory = '/private/remote/path';
|
|
config.build.output_directory = './releases';
|
|
fs.writeFileSync(configFile, stringifyYaml(config));
|
|
fs.writeFileSync(path.join(root, 'server/.env'), 'PRIVATE_TOKEN=secret-local-value\nMC_EULA=TRUE\n');
|
|
commit(root);
|
|
const release = perform(root, { releaseOnly: true });
|
|
assert.equal(path.basename(path.dirname(release)), 'releases');
|
|
const manifest = JSON.parse(fs.readFileSync(path.join(release, 'manifest.json')));
|
|
assert.equal(manifest.git.dirty, false);
|
|
assert.equal(manifest.git.commit, git(root, ['rev-parse', 'HEAD']).toString().trim());
|
|
assert.equal(manifest.server.memory, '3G');
|
|
assert.equal(manifest.server.port, 25570);
|
|
assert(manifest.managed_files.client.some(file => /MouseTweaks/.test(file.path)));
|
|
assert(!manifest.managed_files.server.some(file => /MouseTweaks/.test(file.path)));
|
|
const serverMarker = manifest.managed_files.server.find(file => file.path === 'config/spike-marker.txt');
|
|
assert.equal(serverMarker.owner, 'server');
|
|
assert.equal(serverMarker.overrides, 'pack');
|
|
assert.equal(serverMarker.hashes.sha256, sha(path.join(root, 'server/config/spike-marker.txt')));
|
|
for (const file of manifest.release_files) assert.equal(file.sha256, sha(path.join(release, file.path)));
|
|
const checksumLines = fs.readFileSync(path.join(release, 'SHA256SUMS'), 'utf8').trim().split('\n');
|
|
assert.equal(checksumLines.length, manifest.release_files.length + 1);
|
|
assert(checksumLines.some(line => line.endsWith(' manifest.json')));
|
|
execFileSync('sha256sum', ['-c', 'SHA256SUMS'], { cwd: release });
|
|
const composeText = fs.readFileSync(path.join(release, 'server/compose.yaml'), 'utf8');
|
|
const compose = parseYaml(composeText).services.minecraft;
|
|
assert.equal(compose.image, manifest.server.image);
|
|
assert.equal(compose.environment.MEMORY, '${MC_MEMORY:-3G}');
|
|
assert.equal(compose.ports[0], '${MC_BIND_ADDRESS:-127.0.0.1}:${MC_PORT:-25570}:25565');
|
|
assert.equal(compose.environment.MODRINTH_MODPACK, `/modpacks/${manifest.artifact}`);
|
|
assert(compose.volumes.every(volume => volume.source.startsWith('${MC_')));
|
|
for (const file of ['manifest.json', 'server/compose.yaml', 'server/.env.example']) {
|
|
const text = fs.readFileSync(path.join(release, file), 'utf8');
|
|
for (const secret of ['secret-local-value', 'private-deployment-host', '/private/remote/path', root]) assert(!text.includes(secret));
|
|
}
|
|
assert.match(fs.readFileSync(path.join(release, 'server/.env.example'), 'utf8'), /MC_EULA=FALSE/);
|
|
assert.deepEqual(leftovers(root), []);
|
|
});
|
|
|
|
test('development build records dirty state and leaves source unchanged', t => {
|
|
const root = fixture(t);
|
|
fs.writeFileSync(path.join(root, 'local-note.txt'), 'dirty\n');
|
|
const before = sha(path.join(root, 'pack/pack.toml'));
|
|
const release = perform(root);
|
|
assert.equal(JSON.parse(fs.readFileSync(path.join(release, 'manifest.json'))).git.dirty, true);
|
|
assert.equal(sha(path.join(root, 'pack/pack.toml')), before);
|
|
});
|
|
|
|
test('existing immutable release is never overwritten', t => {
|
|
const root = fixture(t), release = perform(root);
|
|
const before = sha(path.join(release, 'SHA256SUMS'));
|
|
assert.throws(() => perform(root), /already exists/);
|
|
assert.equal(sha(path.join(release, 'SHA256SUMS')), before);
|
|
});
|
|
|
|
test('stale metadata is rejected before export', t => {
|
|
const root = fixture(t);
|
|
fs.appendFileSync(path.join(root, 'pack/config/spike-marker.txt'), 'stale\n');
|
|
assert.throws(() => perform(root), /stale indexed file/);
|
|
});
|
|
|
|
test('refresh changes in temporary copy fail safely without changing source', t => {
|
|
const root = fixture(t), index = path.join(root, 'pack/index.toml'), before = sha(index);
|
|
fs.writeFileSync(path.join(root, '.tools/state.json'), '{"stale":true}');
|
|
assert.throws(() => perform(root), /Stale source/);
|
|
assert.equal(sha(index), before);
|
|
assert.deepEqual(fs.readdirSync(path.join(root, 'dist')), ['.gitkeep']);
|
|
});
|
|
|
|
test('failed export leaves no partial release or build lock', t => {
|
|
const root = fixture(t);
|
|
fs.writeFileSync(path.join(root, '.tools/state.json'), '{"exportFailure":true}');
|
|
assert.throws(() => perform(root), /Simulated export failure/);
|
|
assert.deepEqual(fs.readdirSync(path.join(root, 'dist')), ['.gitkeep']);
|
|
});
|
|
|
|
test('unverified loader/image combination is rejected', t => {
|
|
const root = fixture(t), file = path.join(root, 'pack/pack.toml');
|
|
const pack = parse(fs.readFileSync(file, 'utf8'));
|
|
pack.versions.neoforge = '21.1.251';
|
|
fs.writeFileSync(file, stringify(pack));
|
|
assert.throws(() => perform(root), /not been verified/);
|
|
});
|
|
|
|
test('duplicate mod definitions fail despite a consistent index', t => {
|
|
const root = fixture(t), directory = path.join(root, 'pack');
|
|
const indexFile = path.join(directory, 'index.toml'), index = parse(fs.readFileSync(indexFile, 'utf8'));
|
|
const entry = index.files.find(file => file.file.includes('mouse-tweaks'));
|
|
fs.copyFileSync(path.join(directory, entry.file), path.join(directory, 'mods/duplicate.pw.toml'));
|
|
index.files.push({ ...entry, file: 'mods/duplicate.pw.toml' });
|
|
fs.writeFileSync(indexFile, stringify(index));
|
|
refresh(root);
|
|
assert.throws(() => perform(root), /Duplicate mod target/);
|
|
});
|
|
|
|
test('invalid/duplicate config keys and protected output paths are rejected', t => {
|
|
const root = fixture(t), file = path.join(root, 'platform/config.yaml');
|
|
fs.appendFileSync(file, '\nschema_version: 1\n');
|
|
assert.throws(() => perform(root), /unique/);
|
|
fs.copyFileSync(path.join(repository, 'platform/config.yaml'), file);
|
|
const config = parseYaml(fs.readFileSync(file, 'utf8'));
|
|
config.build.output_directory = './pack/output';
|
|
fs.writeFileSync(file, stringifyYaml(config));
|
|
assert.throws(() => perform(root), /overlaps/);
|
|
config.build.output_directory = '../outside';
|
|
fs.writeFileSync(file, stringifyYaml(config));
|
|
assert.throws(() => perform(root), /inside repository/);
|
|
});
|
|
|
|
test('release mode rejects dirty worktree', t => {
|
|
const root = fixture(t);
|
|
fs.writeFileSync(path.join(root, 'untracked.txt'), 'dirty');
|
|
assert.throws(() => perform(root, { releaseOnly: true }), /clean Git worktree/);
|
|
});
|
|
|
|
test('tracked known secrets are rejected', t => {
|
|
const root = fixture(t);
|
|
fs.writeFileSync(path.join(root, 'server/.env'), 'PRIVATE_TOKEN=x');
|
|
git(root, ['add', '-f', 'server/.env']);
|
|
commit(root);
|
|
assert.throws(() => perform(root), /secret path is tracked/);
|
|
});
|
|
|
|
test('tampered tool and source symlinks fail validation', t => {
|
|
const root = fixture(t);
|
|
const marker = path.join(root, 'pack/config/spike-marker.txt');
|
|
fs.unlinkSync(marker); fs.symlinkSync('/tmp/nonexistent-mcpack-marker', marker);
|
|
assert.throws(() => perform(root), /regular files/);
|
|
fs.unlinkSync(marker); fs.copyFileSync(path.join(repository, 'pack/config/spike-marker.txt'), marker);
|
|
fs.appendFileSync(path.join(root, '.tools/packwiz/packwiz'), '\n// tampered\n');
|
|
assert.throws(() => perform(root), /checksum mismatch/);
|
|
});
|