172 lines
5.6 KiB
TypeScript
172 lines
5.6 KiB
TypeScript
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",
|
|
});
|
|
});
|