168 lines
5.4 KiB
JavaScript
168 lines
5.4 KiB
JavaScript
import assert from "node:assert/strict";
|
|
import { existsSync } from "node:fs";
|
|
import { createHash } from "node:crypto";
|
|
import { createClient } from "@supabase/supabase-js";
|
|
|
|
if (existsSync(".env.local")) process.loadEnvFile(".env.local");
|
|
const origin = new URL(
|
|
process.argv[2] || process.env.MINEGEO_URL || "http://localhost:3000",
|
|
).origin;
|
|
const db = createClient(
|
|
process.env.NEXT_PUBLIC_SUPABASE_URL,
|
|
process.env.SUPABASE_SECRET_KEY,
|
|
{ auth: { persistSession: false, autoRefreshToken: false } },
|
|
);
|
|
let sessionId, cookie;
|
|
async function request(path, body, options = {}) {
|
|
return fetch(origin + path, {
|
|
method: body === undefined ? "GET" : "POST",
|
|
headers: {
|
|
...(body === undefined
|
|
? {}
|
|
: { "content-type": "application/json", origin }),
|
|
...(cookie ? { cookie } : {}),
|
|
...options.headers,
|
|
},
|
|
body: body === undefined ? undefined : JSON.stringify(body),
|
|
signal: AbortSignal.timeout(30000),
|
|
});
|
|
}
|
|
async function json(path, body) {
|
|
const response = await request("/api/v1/" + path, body);
|
|
assert.equal(
|
|
response.status,
|
|
200,
|
|
`${path}: ${await response.clone().text()}`,
|
|
);
|
|
assert.match(response.headers.get("cache-control"), /no-store/);
|
|
return response.json();
|
|
}
|
|
try {
|
|
for (const path of [
|
|
"/",
|
|
"/servers",
|
|
"/downloads",
|
|
"/about",
|
|
"/log-in",
|
|
"/fonts/VT323-Regular.ttf",
|
|
"/images/example-map.webp",
|
|
"/tools/plugin/minegeo-plugin-1.21.11.jar",
|
|
"/tools/mod/minegeo-fabric-1.21.11.jar",
|
|
]) {
|
|
const response = await request(path);
|
|
assert.equal(response.status, 200, path);
|
|
assert.equal(response.headers.get("x-content-type-options"), "nosniff");
|
|
assert.equal(response.headers.get("x-frame-options"), "DENY");
|
|
assert.ok((await response.arrayBuffer()).byteLength > 0, path);
|
|
}
|
|
assert.equal((await json("health")).ok, true);
|
|
for (const path of [
|
|
"/servers/not-a-uuid",
|
|
"/servers/00000000-0000-4000-8000-000000000000",
|
|
"/api/v1/catalog/extra",
|
|
]) {
|
|
assert.equal((await request(path)).status, 404, path);
|
|
}
|
|
for (const path of ["datasets", "tokens", "profile"]) {
|
|
assert.equal(
|
|
(await request("/api/v1/" + path)).status,
|
|
401,
|
|
`guest ${path}`,
|
|
);
|
|
}
|
|
const forbidden = await request(
|
|
"/api/v1/sessions",
|
|
{},
|
|
{ headers: { origin: "https://foreign.example" } },
|
|
);
|
|
assert.equal(forbidden.status, 403);
|
|
const malformed = await fetch(origin + "/api/v1/sessions", {
|
|
method: "POST",
|
|
headers: { "content-type": "application/json", origin },
|
|
body: "{",
|
|
signal: AbortSignal.timeout(30000),
|
|
});
|
|
assert.equal(malformed.status, 400);
|
|
assert.equal((await malformed.json()).error, "Invalid JSON request");
|
|
const catalog = await json("catalog");
|
|
assert.ok(
|
|
catalog.maps.length,
|
|
"Publish at least one map before verifying gameplay",
|
|
);
|
|
const map = catalog.maps[0];
|
|
const created = await request("/api/v1/sessions", {
|
|
datasetId: map.id,
|
|
difficulty: "hard",
|
|
});
|
|
assert.equal(created.status, 200, await created.clone().text());
|
|
sessionId = (await created.json()).id;
|
|
cookie = created.headers.get("set-cookie").split(";")[0];
|
|
const session = await json("sessions/" + sessionId);
|
|
assert.equal(session.rounds.length, 5);
|
|
assert.ok(
|
|
session.rounds.every(
|
|
(round) => round.faces.length === 6 && round.faces.every(Boolean),
|
|
),
|
|
);
|
|
assert.equal(
|
|
(await fetch(origin + "/api/v1/sessions/" + sessionId)).status,
|
|
404,
|
|
"another guest cannot read the session",
|
|
);
|
|
// Exercise actual signed Storage reads, not just the URL response shape.
|
|
for (const url of [session.map, ...session.rounds[0].faces]) {
|
|
const image = await fetch(url, { signal: AbortSignal.timeout(30000) });
|
|
assert.equal(image.status, 200);
|
|
assert.match(image.headers.get("content-type"), /image\/webp/);
|
|
assert.ok((await image.arrayBuffer()).byteLength > 0);
|
|
}
|
|
for (let round = 0; round < 5; round++) {
|
|
const first = await json(`sessions/${sessionId}/activate`, { round });
|
|
const duplicate = await json(`sessions/${sessionId}/activate`, { round });
|
|
assert.equal(
|
|
first.deadline,
|
|
duplicate.deadline,
|
|
"activation does not reset the timer",
|
|
);
|
|
assert.equal(
|
|
(
|
|
await request(`/api/v1/sessions/${sessionId}/guess`, {
|
|
round,
|
|
x: map.bounds.maxX + 1,
|
|
z: map.bounds.minZ,
|
|
})
|
|
).status,
|
|
400,
|
|
);
|
|
const guess = {
|
|
round,
|
|
x: (map.bounds.minX + map.bounds.maxX) / 2,
|
|
z: (map.bounds.minZ + map.bounds.maxZ) / 2,
|
|
};
|
|
const result = await json(`sessions/${sessionId}/guess`, guess);
|
|
assert.ok(result.score >= 0 && result.score <= 5000);
|
|
assert.equal(result.timedOut, false);
|
|
assert.deepEqual(
|
|
await json(`sessions/${sessionId}/guess`, guess),
|
|
result,
|
|
"submission retries are idempotent",
|
|
);
|
|
}
|
|
assert.equal((await json("sessions/" + sessionId)).round, 5);
|
|
console.log(
|
|
"Production HTTP/API/Storage smoke passed: pages, headers, privacy, origin checks, signed media and all five rounds.",
|
|
);
|
|
} finally {
|
|
if (sessionId && cookie) {
|
|
const secret = cookie.slice(cookie.indexOf("=") + 1);
|
|
const { error } = await db
|
|
.from("game_sessions")
|
|
.delete()
|
|
.eq("id", sessionId)
|
|
.eq("access_hash", createHash("sha256").update(secret).digest("hex"))
|
|
.is("owner_id", null);
|
|
if (error)
|
|
throw new Error("Smoke session cleanup failed: " + error.message);
|
|
}
|
|
}
|