import { test, type TestContext } from "node:test"; import assert from "node:assert/strict"; import { mkdtemp, mkdir, readFile, rm, writeFile } from "node:fs/promises"; import { resolve, sep } from "node:path"; import { createHash } from "node:crypto"; import { zipSync } from "fflate"; import sharp from "sharp"; import { prepareAssets, assetsRoot } from "../scripts/assets.mjs"; const png = await sharp({ create: { width: 16, height: 16, channels: 3, background: "#719447" }, }) .png() .toBuffer(); const json = Buffer.from('{"variants":{},"elements":[],"textures":{}}'); const files = { "blockstates/grass_block.json": json, "blockstates/stone.json": json, "models/block/block.json": json, "models/block/cube.json": json, "models/block/cube_all.json": json, "textures/block/grass_block_top.png": png, "textures/block/dirt.png": png, "textures/block/stone.png": png, "textures/block/water_still.png": png, "textures/block/water_still.png.mcmeta": Buffer.from( '{"animation":{"frametime":2}}', ), }; const jar = zipSync( Object.fromEntries( Object.entries(files).map(([path, bytes]) => [ `assets/minecraft/${path}`, bytes, ]), ), ); const checksum = createHash("sha1").update(jar).digest("hex"); async function isolatedCache( t: TestContext, corruptDownload = false, minecraftVersion = "1.21.11", ) { const previous = process.cwd(); const cacheParent = resolve(".cache"); await mkdir(cacheParent, { recursive: true }); const directory = await mkdtemp(`${cacheParent}/asset-tests-`); process.chdir(directory); t.after(async () => { process.chdir(previous); assert.ok(resolve(directory).startsWith(cacheParent + sep)); await rm(directory, { recursive: true, force: true }); }); const requests: string[] = []; t.mock.method(globalThis, "fetch", async (input: string | URL | Request) => { const url = String(input); requests.push(url); if (url.includes("version_manifest_v2")) return Response.json({ versions: [ { id: minecraftVersion, url: "https://fixture.invalid/version" }, ], }); if (url.endsWith("/version")) return Response.json({ downloads: { client: { url: "https://fixture.invalid/client.jar", sha1: checksum }, }, }); if (url.endsWith("/client.jar")) return new Response( Buffer.from(corruptDownload ? "incomplete download" : jar), ); throw new Error(`Unexpected request: ${url}`); }); return requests; } test("missing vanilla cache is prepared completely and subsequent runs work offline", async (t) => { const requests = await isolatedCache(t); await prepareAssets(); assert.equal(requests.length, 3); for (const [path, expected] of Object.entries(files)) { assert.deepEqual( await readFile(`${assetsRoot}/assets/minecraft/${path}`), expected, ); } const marker = JSON.parse( await readFile(`${assetsRoot}/.minegeo-assets.json`, "utf8"), ); assert.equal(marker.files, Object.keys(files).length); assert.equal(marker.clientSha1, checksum); requests.length = 0; await prepareAssets(); assert.deepEqual(requests, []); }); test("26.x assets use distinct verified caches and reject unsupported versions", async (t) => { await isolatedCache(t, false, "26.2"); const directory = await prepareAssets("26.2"); assert.equal(directory, ".cache/minecraft/26.2"); const marker = JSON.parse( await readFile(`${directory}/.minegeo-assets.json`, "utf8"), ); assert.equal(marker.minecraftVersion, "26.2"); await assert.rejects(readFile(`${assetsRoot}/.minegeo-assets.json`), { code: "ENOENT", }); await assert.rejects( prepareAssets("../secrets"), /Unsupported Minecraft asset version/, ); await assert.rejects( prepareAssets("26.3"), /Unsupported Minecraft asset version/, ); }); test("an interrupted extraction with only a blockstate cannot masquerade as complete", async (t) => { await isolatedCache(t); await mkdir(`${assetsRoot}/assets/minecraft/blockstates`, { recursive: true, }); await writeFile( `${assetsRoot}/assets/minecraft/blockstates/grass_block.json`, json, ); await prepareAssets(); assert.deepEqual( await readFile(`${assetsRoot}/assets/minecraft/textures/block/stone.png`), png, ); }); test("a complete marker with a missing texture is repaired from the verified cached JAR", async (t) => { const requests = await isolatedCache(t); await prepareAssets(); const path = `${assetsRoot}/assets/minecraft/textures/block/stone.png`; await rm(path); requests.length = 0; await prepareAssets(); assert.deepEqual(await readFile(path), png); assert.equal(requests.filter((url) => url.endsWith("client.jar")).length, 0); }); test("a corrupted cached client is downloaded again", async (t) => { const requests = await isolatedCache(t); await mkdir(".cache/minecraft", { recursive: true }); await writeFile(".cache/minecraft/client-1.21.11.jar", "corrupted"); await prepareAssets(); assert.equal(requests.filter((url) => url.endsWith("client.jar")).length, 1); assert.equal( createHash("sha1") .update(await readFile(".cache/minecraft/client-1.21.11.jar")) .digest("hex"), checksum, ); }); test("a failed checksum never creates a completion marker or saves a downloaded client", async (t) => { await isolatedCache(t, true); await assert.rejects(prepareAssets(), /checksum mismatch/); await assert.rejects(readFile(`${assetsRoot}/.minegeo-assets.json`), { code: "ENOENT", }); await assert.rejects(readFile(".cache/minecraft/client-1.21.11.jar"), { code: "ENOENT", }); });