Add account based multiplayer hosting and bug report retention
This commit is contained in:
@@ -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();
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user