import assert from "node:assert/strict"; import { randomUUID } from "node:crypto"; import { existsSync } from "node:fs"; 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; const db = createClient( process.env.NEXT_PUBLIC_SUPABASE_URL, process.env.SUPABASE_SECRET_KEY, { auth: { persistSession: false, autoRefreshToken: false } }, ); const rooms = []; function player() { let cookie; return async (path, body, expected = 200) => { const response = await fetch(`${origin}/api/v1/${path}`, { method: body === undefined ? "GET" : "POST", headers: { ...(body === undefined ? {} : { origin, "content-type": "application/json" }), ...(cookie ? { cookie } : {}), }, 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]; 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; }; } try { const host = player(), friend = player(), outsider = player(); const catalog = await host("catalog"); const datasetId = catalog.maps[0]?.id; assert.ok(datasetId, "A published map is required"); const created = await host("rooms", { datasetId, difficulty: "hard", displayName: "HTTP Host", playerLimit: 2, }); rooms.push(created.id); const path = `rooms/${created.id}`; const hostView = await host(path); const preview = await outsider(path); assert.equal(preview.you, null); assert.deepEqual(preview.rounds, []); await outsider(`${path}/media`, undefined, 403); const joined = await friend(`${path}/join`, { displayName: "HTTP Friend" }); await outsider(`${path}/chat`, undefined, 403); await outsider( `${path}/chat`, { text: "Intruder", clientId: randomUUID() }, 403, ); const sent = { text: "HTTP hello plain text", clientId: randomUUID(), name: "Forged", }; const messages = (await host(`${path}/chat`, sent)).messages; assert.equal( messages[0].name, "HTTP Host", "sender name is server-controlled", ); assert.equal(messages[0].playerId, hostView.you); assert.equal((await host(`${path}/chat`, sent)).messages.length, 1); assert.deepEqual((await friend(`${path}/chat`)).messages, messages); await friend(`${path}/chat`, { text: " ", clientId: randomUUID() }, 400); await friend( `${path}/chat`, { text: "x".repeat(501), clientId: randomUUID() }, 400, ); await friend( `${path}/chat`, { text: "x\u202e", clientId: randomUUID() }, 400, ); const concurrentMessages = await Promise.allSettled([ friend(`${path}/chat`, { text: "First simultaneous send", clientId: randomUUID(), }), friend(`${path}/chat`, { text: "Second simultaneous send", clientId: randomUUID(), }), ]); assert.equal( concurrentMessages.filter((result) => result.status === "fulfilled").length, 1, "only one simultaneous new message succeeds", ); assert.match( concurrentMessages.find((result) => result.status === "rejected").reason .message, /Too many chat messages/, ); assert.equal( (await friend(`${path}/join`, { displayName: "HTTP Other" })).you, joined.you, ); await outsider(`${path}/join`, { displayName: "Overflow" }, 400); await friend(`${path}/start`, {}, 403); await host(`${path}/start`, {}); let results; for (let round = 0; round < 5; round++) { const [hostMedia, friendMedia] = await Promise.all([ host(`${path}/media`), friend(`${path}/media`), ]); assert.equal(hostMedia.round, round); assert.equal(friendMedia.round, round); assert.deepEqual( hostMedia.faces.map((url) => new URL(url).pathname), friendMedia.faces.map((url) => new URL(url).pathname), ); assert.equal(hostMedia.faces.length, 6); if (round === 0) for (const url of [hostMedia.map, hostMedia.faces[0]]) assert.equal((await fetch(url)).status, 200, "Signed media readable"); await host(`${path}/ready`, { round }); const playing = await friend(`${path}/ready`, { round }); assert.equal(playing.status, "playing"); const synchronized = await host(path); assert.equal( synchronized.rounds[round].deadline, playing.rounds[round].deadline, ); const pointResult = await db .from("multiplayer_rounds") .select("viewpoint") .eq("room_id", created.id) .eq("round_number", round) .single(); if (pointResult.error) throw pointResult.error; const point = pointResult.data.viewpoint; const first = await host(`${path}/guess`, { round, x: point.x, z: point.z, }); assert.equal(first.status, "playing"); assert.deepEqual(first.rounds[round].results, []); assert.equal( (await host(path)).players.find((p) => p.id === hostView.you).submitted, true, "Reconnect preserves submitted guess", ); await host(`${path}/guess`, { round, x: hostMedia.bounds.minX, z: hostMedia.bounds.minZ, }); const b = hostMedia.bounds; const x = point.x - (b.minX + b.maxX) / 2 >= 0 ? b.minX : b.maxX; const z = point.z - (b.minZ + b.maxZ) / 2 >= 0 ? b.minZ : b.maxZ; results = await friend(`${path}/guess`, { round, x, z }); assert.equal(results.status, round === 4 ? "finished" : "results"); assert.equal( results.rounds[round].results.find((r) => r.playerId === hostView.you) .score, 5000, ); if (round < 4) await host(`${path}/next`, { round }); } assert.equal(results.players[0].id, hostView.you); assert.equal(results.players[0].total, 25000); assert.equal( (await host(`${path}/chat`)).messages.length, 2, "messages persist over five rounds", ); await friend(`${path}/leave`, {}); await friend(`${path}/chat`, undefined, 403); await friend( `${path}/chat`, { text: "After leaving", clientId: randomUUID() }, 403, ); assert.equal(results.rounds.flatMap((round) => round.results).length, 10); const capacityHost = player(); const full = await capacityHost("rooms", { datasetId, difficulty: "normal", displayName: "Capacity Host", playerLimit: 16, }); rooms.push(full.id); await Promise.all( Array.from({ length: 14 }, (_, i) => player()(`rooms/${full.id}/join`, { displayName: `Capacity ${i + 2}` }), ), ); const attempts = await Promise.allSettled([ player()(`rooms/${full.id}/join`, { displayName: "Last A" }), player()(`rooms/${full.id}/join`, { displayName: "Last B" }), ]); assert.equal( attempts.filter((result) => result.status === "fulfilled").length, 1, "Exactly one of the simultaneous last-slot joins succeeds", ); 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); 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.", ); } finally { if (rooms.length) { const { error } = await db .from("multiplayer_rooms") .delete() .in("id", rooms); if (error) throw new Error(`Multiplayer test-room cleanup failed: ${error.message}`); } }