import assert from "node:assert/strict"; import { randomUUID } from "node:crypto"; import { readFile, readdir } from "node:fs/promises"; import { PGlite } from "../.data/sql-verification/node_modules/@electric-sql/pglite/dist/index.js"; // Isolated PostgreSQL: fixtures never enter production rooms or leaderboards. const db = new PGlite(); try { await db.exec(`create role anon; create role authenticated; create role service_role; create schema auth; create table auth.users(id uuid primary key); create function auth.uid() returns uuid language sql as $$ select null::uuid $$; create schema storage; create table storage.buckets(id text primary key,name text,public boolean,file_size_limit bigint);`); for (const file of (await readdir("supabase/migrations")) .filter((file) => file.endsWith(".sql")) .sort()) await db.exec(await readFile(`supabase/migrations/${file}`, "utf8")); const account = ( await db.query( "insert into auth.users values(gen_random_uuid()) returning id", ) ).rows[0].id; const dataset = ( await db.query( `insert into public.datasets(owner_id,name,source,bounds,state,panorama_count) values($1,'Multiplayer fixture','plugin','{"minX":0,"minZ":0,"maxX":64,"maxZ":64}','published',5) returning id`, [account], ) ).rows[0].id; const manifest = { name: "Multiplayer fixture", bounds: { minX: 0, minZ: 0, maxX: 64, maxZ: 64 }, viewpoints: Array.from({ length: 5 }, (_, i) => ({ id: `point${i}`, x: 10 + i, z: 10 + i, y: 64, })), }; const revision = ( await db.query( "insert into public.revisions(dataset_id,asset_root,manifest) values($1,'fixture',$2) returning id", [dataset, JSON.stringify(manifest)], ) ).rows[0].id; await db.query("update public.datasets set revision_id=$1 where id=$2", [ revision, dataset, ]); const secret = (n) => String(n).padStart(64, "0"); async function create(cap = 2) { return ( await db.query( "select public.create_multiplayer_room($1,'hard','Host',$2,$3) as id", [dataset, cap, secret(1)], ) ).rows[0].id; } async function act(room, who, type = "view", payload = {}) { return ( await db.query("select public.multiplayer_action($1,$2,$3,$4) as room", [ room, secret(who), type, JSON.stringify(payload), ]) ).rows[0].room; } for (const cap of [1, 17, null]) await assert.rejects(create(cap), /Choose between 2 and 16 players/); const room = await create(); let view = await act(room, 1); const host = view.you; assert.equal(view.playerLimit, 2); assert.equal(view.players.length, 1); assert.equal(view.rounds.length, 5); assert.ok(view.rounds.every((turn) => turn.results.length === 0)); const outsider = await act(room, 3); assert.equal(outsider.you, null); assert.deepEqual(outsider.players, []); assert.deepEqual(outsider.rounds, []); assert.ok(!JSON.stringify(view).includes('"viewpoint"')); assert.ok(!JSON.stringify(view).includes("access_hash")); await assert.rejects(act(room, 1, "start"), /Invite at least one friend/); await assert.rejects(act(room, 3, "start"), /membership permission/); const joined = await act(room, 2, "join", { name: "Friend" }); const friend = joined.you; const chat = async (roomId, who, text = null, clientId = null) => ( await db.query("select public.multiplayer_chat($1,$2,$3,$4) as chat", [ roomId, secret(who), text, clientId, ]) ).rows[0].chat; const messageId = randomUUID(); let messages = (await chat(room, 1, "Hello, friend!", messageId)).messages; assert.equal(messages[0].name, "Host"); assert.equal(messages[0].playerId, host); assert.equal(messages[0].text, "Hello, friend!"); assert.equal(typeof messages[0].id, "string"); assert.deepEqual((await chat(room, 2)).messages, messages); assert.equal( (await chat(room, 1, "Hello, friend!", messageId)).messages.length, 1, "retry is idempotent", ); await assert.rejects(chat(room, 1, "Spam", randomUUID()), /Too many chat/); await assert.rejects(chat(room, 3), /Chat permission/); await assert.rejects(chat(room, 3, "Spoof", randomUUID()), /Chat permission/); for (const text of [" ", "x".repeat(501), "x\nname", "x\u202e", "x\u0085"]) await assert.rejects(chat(room, 2, text, randomUUID()), /Invalid chat/); await assert.rejects(chat(room, 2, "Missing nonce"), /Invalid chat/); messages = ( await chat(room, 2, "Привет (: ", randomUUID()) ).messages; assert.equal(messages[1].name, "Friend"); assert.equal(messages[1].text, "Привет (: "); assert.ok(!JSON.stringify(messages).includes("access_hash")); assert.ok(!JSON.stringify(messages).includes("viewpoint")); const otherRoom = await create(); assert.deepEqual( (await chat(otherRoom, 1)).messages, [], "room histories stay separate", ); assert.equal( (await act(room, 2, "join", { name: "Changed name" })).you, friend, ); await assert.rejects(act(room, 3, "join", { name: "Extra" }), /room is full/); await assert.rejects(act(room, 2, "start"), /owner permission/); view = await act(room, 1, "start"); assert.equal(view.status, "loading"); await assert.rejects( act(room, 3, "join", { name: "Late" }), /already started/, ); await assert.rejects( act(room, 1, "guess", { round: 0, x: 10, z: 10 }), /Round is not active/, ); assert.equal((await act(room, 1, "ready", { round: 0 })).status, "loading"); view = await act(room, 2, "ready", { round: 0 }); assert.equal(view.status, "playing"); const deadline = view.rounds[0].deadline; assert.equal( (await chat(room, 1)).messages.length, 2, "chat persists into the game", ); assert.equal( (await act(room, 1, "ready", { round: 0 })).rounds[0].deadline, deadline, ); await assert.rejects( act(room, 1, "guess", { round: 0, x: 65, z: 0 }), /Guess outside map/, ); const location = ( await db.query( "select viewpoint from public.multiplayer_rounds where room_id=$1 and round_number=0", [room], ) ).rows[0].viewpoint; view = await act(room, 1, "guess", { round: 0, x: location.x, z: location.z, }); assert.equal(view.status, "playing"); assert.deepEqual(view.rounds[0].results, []); assert.ok( view.players.every((player) => player.total === 0), "scores concealed until everyone finishes", ); assert.deepEqual(view.myGuess, { x: location.x, z: location.z }); await act(room, 1, "guess", { round: 0, x: 0, z: 0 }); assert.equal( ( await db.query( "select count(*)::integer as count from public.multiplayer_guesses where room_id=$1 and player_id=$2", [room, host], ) ).rows[0].count, 1, ); view = await act(room, 2, "guess", { round: 0, x: 0, z: 0 }); assert.equal(view.status, "results"); assert.equal( view.rounds[0].results.find((result) => result.playerId === host).score, 5000, ); assert.ok( view.rounds[0].results.find((result) => result.playerId === friend).score < 5000, ); await assert.rejects(act(room, 2, "next", { round: 0 }), /owner permission/); assert.equal( (await act(room, 1, "guess", { round: 0, x: 0, z: 0 })).status, "results", "retry cannot alter a submitted guess", ); for (let round = 1; round < 5; round++) { await act(room, 1, "next", { round: round - 1 }); await act(room, 1, "ready", { round }); await act(room, 2, "ready", { round }); const point = ( await db.query( "select viewpoint from public.multiplayer_rounds where room_id=$1 and round_number=$2", [room, round], ) ).rows[0].viewpoint; await act(room, 1, "guess", { round, x: point.x, z: point.z }); if (round === 2) { await db.query( "update public.multiplayer_rounds set deadline=clock_timestamp()-interval '1 second' where room_id=$1 and round_number=$2", [room, round], ); view = await act(room, 2, "guess", { round, x: point.x, z: point.z }); assert.equal( view.rounds[round].results.find((result) => result.playerId === friend) .score, 0, ); assert.equal( view.rounds[round].results.find((result) => result.playerId === friend) .timedOut, true, ); } else view = await act(room, 2, "guess", { round, x: 0, z: 0 }); } assert.equal(view.status, "finished"); assert.equal( (await chat(room, 2)).messages.length, 2, "chat remains available at final standings", ); assert.equal(view.players[0].id, host); assert.equal(view.players[0].total, 25000); assert.equal(view.players[0].rank, 1); assert.equal(view.players[1].rank, 2); assert.equal(view.rounds.flatMap((turn) => turn.results).length, 10); const capacity = await create(16); for (let n = 2; n <= 16; n++) await act(capacity, n, "join", { name: `Player ${n}` }); await assert.rejects( act(capacity, 17, "join", { name: "Overflow" }), /room is full/, ); assert.equal((await act(capacity, 1)).playerCount, 16); const removed = (await act(capacity, 16)).you; await act(capacity, 1, "kick", { playerId: removed }); await assert.rejects(chat(capacity, 16), /Chat permission/); assert.equal((await act(capacity, 16)).you, null); await act(capacity, 17, "join", { name: "Replacement" }); await act(capacity, 1, "leave"); await assert.rejects( chat(capacity, 1, "Left", randomUUID()), /Chat permission/, ); assert.equal( (await act(capacity, 2)).hostId, (await act(capacity, 2)).you, "ownership transfers when host leaves", ); await assert.rejects( act(capacity, 18, "join", { name: "player 2" }), /name is already taken/, ); const stalled = await create(); await act(stalled, 2, "join", { name: "Offline" }); await act(stalled, 1, "start"); await act(stalled, 1, "ready", { round: 0 }); await db.query( "update public.multiplayer_rooms set transition_at=clock_timestamp()-interval '1 second' where id=$1", [stalled], ); assert.equal( (await act(stalled, 1)).status, "playing", "offline loader cannot block the match", ); await db.query( "update public.multiplayer_rounds set deadline=clock_timestamp()-interval '1 second' where room_id=$1 and round_number=0", [stalled], ); view = await act(stalled, 1); assert.equal(view.status, "results"); assert.ok( view.players.every((player) => player.rank === 1), "equal totals share a rank", ); await db.query( "update public.multiplayer_rooms set transition_at=clock_timestamp()-interval '1 second' where id=$1", [stalled], ); assert.equal( (await act(stalled, 1)).round, 1, "rounds progress without host clicks", ); await act(stalled, 2, "leave"); await act(stalled, 1, "ready", { round: 1 }); const point = ( await db.query( "select viewpoint from public.multiplayer_rounds where room_id=$1 and round_number=1", [stalled], ) ).rows[0].viewpoint; view = await act(stalled, 1, "guess", { round: 1, x: point.x, z: point.z }); assert.equal(view.status, "results", "departed players cannot block scoring"); await act(stalled, 1, "close"); await assert.rejects(chat(stalled, 1), /closed or expired/); assert.equal((await act(stalled, 1)).status, "closed"); const expired = await create(); await db.query( "update public.multiplayer_rooms set created_at=now()-interval '25 hours' where id=$1", [expired], ); await assert.rejects(chat(expired, 1), /closed or expired/); const history = await create(); const historyHost = (await act(history, 1)).you; await db.query( `insert into public.multiplayer_messages(room_id,player_id,client_id,display_name,body,created_at) select $1,$2,gen_random_uuid(),'Host','message ' || n,now()-interval '2 minutes' from generate_series(1,120) n`, [history, historyHost], ); const recent = (await chat(history, 1)).messages; assert.equal(recent.length, 100); assert.equal(recent[0].text, "message 21"); assert.equal(recent[99].text, "message 120"); await db.query( "update public.multiplayer_messages set created_at=clock_timestamp()-interval '2 seconds' where room_id=$1", [history], ); await assert.rejects( chat(history, 1, "Minute flood", randomUUID()), /Too many chat/, ); for (const role of ["anon", "authenticated"]) { await db.exec(`set role ${role}`); for (const table of [ "multiplayer_rooms", "multiplayer_players", "multiplayer_rounds", "multiplayer_guesses", "multiplayer_messages", ]) await assert.rejects( db.query(`select * from public.${table}`), /permission denied/, ); await assert.rejects(act(room, 1), /permission denied/); await assert.rejects(chat(room, 1), /permission denied/); await assert.rejects(create(), /permission denied/); await db.exec("reset role"); } console.log( "Multiplayer SQL checks passed: room chat privacy, membership revocation, message validation, bounded history, rate limits, retries, expiration, 16-player cap, synchronized rounds, and client-role isolation.", ); } finally { await db.close(); }