Add test coverage retrofit for Phase 1/2 (worker, api, frontend)

Requested after Phase 2: from here on, MCMapper development follows
TDD (test-first) — this retrofits the pieces already built before that
request landed.

worker: unit tests for the tile rasterizer (background fill, exact
upscaled-block boundaries, full-grid painting, out-of-bounds columns) and
the block-color palette (distinctness checks, including that the
"unmapped block" placeholder never accidentally collides with a real
block's color). 16 tests total alongside the existing mesher tests.

api: wired up `bun test`. Unit tests for chunkOf's coordinate math.
Integration tests (real Postgres/Redis/MinIO, see README's new "Running
tests" section) for wsGateway.message() — auth accept/reject, upsert +
dedup on columns/sections, not-authenticated/invalid-JSON handling — and
for the tile/mesh/servers HTTP routes, driven through Elysia's in-process
`.handle()` rather than a bound port (sidesteps the stale dev-server
port-collision issue hit repeatedly this session). index.ts now exports
`app` and only calls `.listen()` when run directly, specifically so tests
can drive it this way.

frontend: extracted mesh.js's binary-format parser into its own ESM
module (mesh-format.js) so it's unit-testable without a browser/Babylon;
mesh.js now imports it. Tests build a buffer independently of the parser
(mirroring worker's encoder layout) so a mismatch in either direction —
Rust producer or JS consumer drifting — would be caught.
This commit is contained in:
2026-08-08 16:39:27 +02:00
parent 5ed4d32a56
commit dc7185c15e
16 changed files with 623 additions and 38 deletions
+128
View File
@@ -0,0 +1,128 @@
import { describe, test, expect, beforeAll, afterAll } from "bun:test";
import { app } from "./index";
import { db } from "./db/client";
import { meshPointers, tilePointers } from "./db/schema";
import { minio, TILE_BUCKET } from "./minio";
import { createTestServer, deleteTestServer } from "./test-helpers";
// Elysia's `.handle()` drives the app in-process against a plain Request/Response, without
// binding a real port — avoids racing a real running instance for the port (see MCMapper's
// memory notes on stale dev-server processes double-binding a port on Windows) and is faster.
function get(path: string) {
return app.handle(new Request(`http://localhost${path}`));
}
describe("GET /health", () => {
test("reports ok", async () => {
const res = await get("/health");
expect(res.status).toBe(200);
expect(await res.json()).toEqual({ status: "ok" });
});
});
describe("GET /api/servers", () => {
let server: { id: string; token: string };
beforeAll(async () => {
server = await createTestServer("servers-route");
});
afterAll(async () => {
await deleteTestServer(server.id);
});
test("includes the seeded server without leaking its token", async () => {
const res = await get("/api/servers");
expect(res.status).toBe(200);
const rows = (await res.json()) as any[];
const found = rows.find((r) => r.id === server.id);
expect(found).toEqual({ id: server.id, name: "test-servers-route" });
});
});
describe("GET /api/tiles/:serverId/:dimension/:zoom/:tileX/:tileY", () => {
let server: { id: string; token: string };
const storageKey = "test-tile-object.png";
const pngBytes = new Uint8Array([1, 2, 3, 4]);
beforeAll(async () => {
server = await createTestServer("tiles-route");
await minio.putObject(TILE_BUCKET, storageKey, Buffer.from(pngBytes));
await db.insert(tilePointers).values({
serverId: server.id,
dimension: 0,
zoom: 0,
tileX: 5,
tileZ: -3, // stored as chunkZ; the route negates the requested tileY to get here
storageKey,
contentHash: "test-hash",
});
});
afterAll(async () => {
await deleteTestServer(server.id);
await minio.removeObject(TILE_BUCKET, storageKey).catch(() => {});
});
test("negates tileY back to the stored chunkZ and streams the object", async () => {
// chunkZ = -3, so the frontend's tileY (see public/js/map.js) is 3.
const res = await get(`/api/tiles/${server.id}/0/0/5/3.png`);
expect(res.status).toBe(200);
expect(res.headers.get("content-type")).toBe("image/png");
const body = new Uint8Array(await res.arrayBuffer());
expect(body).toEqual(pngBytes);
});
test("404s for a tile that hasn't been rendered", async () => {
const res = await get(`/api/tiles/${server.id}/0/0/999/999.png`);
expect(res.status).toBe(404);
});
});
describe("GET /api/meshes/...", () => {
let server: { id: string; token: string };
const storageKey = "test-mesh-object.bin";
const meshBytes = new Uint8Array([9, 9, 9]);
beforeAll(async () => {
server = await createTestServer("meshes-route");
await minio.putObject(TILE_BUCKET, storageKey, Buffer.from(meshBytes));
await db.insert(meshPointers).values({
serverId: server.id,
dimension: 0,
x: 2,
z: 3,
sectionY: 4,
storageKey,
contentHash: "test-hash",
});
});
afterAll(async () => {
await deleteTestServer(server.id);
await minio.removeObject(TILE_BUCKET, storageKey).catch(() => {});
});
test("lists the rendered sections for a chunk", async () => {
const res = await get(`/api/meshes/${server.id}/0/2/3`);
expect(await res.json()).toEqual([4]);
});
test("lists nothing for a chunk with no meshes yet", async () => {
const res = await get(`/api/meshes/${server.id}/0/999/999`);
expect(await res.json()).toEqual([]);
});
test("serves the mesh binary with the right content type", async () => {
const res = await get(`/api/meshes/${server.id}/0/2/3/4.bin`);
expect(res.status).toBe(200);
expect(res.headers.get("content-type")).toBe("application/octet-stream");
const body = new Uint8Array(await res.arrayBuffer());
expect(body).toEqual(meshBytes);
});
test("404s for a section that hasn't been rendered", async () => {
const res = await get(`/api/meshes/${server.id}/0/2/3/15.bin`);
expect(res.status).toBe(404);
});
});