Add account based multiplayer hosting and bug report retention

This commit is contained in:
2026-10-10 16:24:34 +05:00
parent 9af71c85c2
commit fc6da6756d
33 changed files with 1538 additions and 102 deletions

View File

@@ -1,6 +1,7 @@
import assert from "node:assert/strict";
import { randomUUID } from "node:crypto";
import { randomUUID, randomBytes } from "node:crypto";
import { existsSync } from "node:fs";
import { mkdir, writeFile } from "node:fs/promises";
import { createClient } from "@supabase/supabase-js";
if (existsSync(".env.local")) process.loadEnvFile(".env.local");
const origin = new URL(process.argv[2] || "http://127.0.0.1:3007").origin;
@@ -9,9 +10,20 @@ const db = createClient(
process.env.SUPABASE_SECRET_KEY,
{ auth: { persistSession: false, autoRefreshToken: false } },
);
const rooms = [];
const rooms = [],
accounts = [];
await mkdir(".data/multiplayer-check", { recursive: true });
const journal = () =>
writeFile(
".data/multiplayer-check/http-fixtures.json",
JSON.stringify({ rooms, accounts }),
);
const checked = (result) => {
if (result.error) throw new Error(result.error.message);
return result.data;
};
function player() {
let cookie;
const cookies = new Map();
return async (path, body, expected = 200) => {
const response = await fetch(`${origin}/api/v1/${path}`, {
method: body === undefined ? "GET" : "POST",
@@ -19,35 +31,88 @@ function player() {
...(body === undefined
? {}
: { origin, "content-type": "application/json" }),
...(cookie ? { cookie } : {}),
cookie: [...cookies]
.map(([key, value]) => `${key}=${value}`)
.join("; "),
},
body: body === undefined ? undefined : JSON.stringify(body),
signal: AbortSignal.timeout(30000),
});
if (response.headers.get("set-cookie"))
cookie = response.headers.get("set-cookie").split(";")[0];
for (const cookie of response.headers.getSetCookie()) {
const part = cookie.split(";")[0],
at = part.indexOf("=");
cookies.set(part.slice(0, at), part.slice(at + 1));
}
const data = await response.json();
assert.equal(response.status, expected, JSON.stringify({ path, data }));
assert.match(response.headers.get("cache-control"), /no-store/);
return data;
};
}
async function account(displayName) {
const email = `multiplayer-check-${randomUUID()}@example.com`,
password = randomBytes(24).toString("base64url");
const { user } = checked(
await db.auth.admin.createUser({
email,
password,
email_confirm: true,
user_metadata: { display_name: "Stale metadata name" },
}),
);
accounts.push(user.id);
await journal();
checked(
await db
.from("profiles")
.insert({ id: user.id, display_name: displayName }),
);
const api = player();
await api("auth", { action: "login", email, password });
return api;
}
try {
const host = player(),
const host = await account("HTTP Host"),
friend = player(),
outsider = player();
const catalog = await host("catalog");
const datasetId = catalog.maps[0]?.id;
assert.ok(datasetId, "A published map is required");
await outsider(
"rooms",
{ datasetId, difficulty: "normal", playerLimit: 2 },
401,
);
await host(
"rooms",
{
datasetId,
difficulty: "hard",
playerLimit: 2,
displayName: "Forged host",
},
400,
);
const created = await host("rooms", {
datasetId,
difficulty: "hard",
displayName: "HTTP Host",
playerLimit: 2,
});
rooms.push(created.id);
await journal();
const path = `rooms/${created.id}`;
const hostView = await host(path);
assert.equal(
hostView.players.find((p) => p.id === hostView.hostId).name,
"HTTP Host",
"host uses stored profile, not stale auth metadata",
);
const rejoined = await host(`${path}/join`, { displayName: "Forged host" });
assert.equal(
rejoined.players.find((p) => p.id === rejoined.hostId).name,
"HTTP Host",
"host rejoin cannot rename the account",
);
const preview = await outsider(path);
assert.equal(preview.you, null);
assert.deepEqual(preview.rounds, []);
@@ -187,14 +252,14 @@ try {
403,
);
assert.equal(results.rounds.flatMap((round) => round.results).length, 10);
const capacityHost = player();
const capacityHost = await account("Capacity Host");
const full = await capacityHost("rooms", {
datasetId,
difficulty: "normal",
displayName: "Capacity Host",
playerLimit: 16,
});
rooms.push(full.id);
await journal();
await Promise.all(
Array.from({ length: 14 }, (_, i) =>
player()(`rooms/${full.id}/join`, { displayName: `Capacity ${i + 2}` }),
@@ -212,6 +277,11 @@ try {
const rejection = attempts.find((result) => result.status === "rejected");
assert.match(rejection.reason.message, /This room is full/);
assert.equal((await capacityHost(`rooms/${full.id}`)).playerCount, 16);
assert.equal(
(await capacityHost(`rooms/${full.id}/leave`, {})).status,
"closed",
"guests cannot become party hosts",
);
console.log(
"Multiplayer HTTP smoke passed: private room chat, trusted sender names, validation, concurrent-send throttling, retries, revoked membership, five shared rounds, signed media, and simultaneous 16-player capacity checks.",
);
@@ -223,5 +293,13 @@ try {
.in("id", rooms);
if (error)
throw new Error(`Multiplayer test-room cleanup failed: ${error.message}`);
rooms.length = 0;
await journal();
}
for (const id of accounts) {
checked(await db.from("profiles").delete().eq("id", id));
checked(await db.auth.admin.deleteUser(id));
}
accounts.length = 0;
await journal();
}