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,allowed_mime_types text[]); create table storage.objects(id uuid primary key,bucket_id text,name text,owner_id text); create function storage.foldername(text) returns text[] language sql as $$ select string_to_array($1, '/') $$;`); 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_map_room($1,'hard','Host',$2,$3,$4) as id", [dataset, cap, secret(1), account], ) ).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; } await db.query( "insert into public.profiles(id,display_name) values($1,'Account host')", [account], ); const secondAccount = ( await db.query( "insert into auth.users values(gen_random_uuid()) returning id", ) ).rows[0].id; await db.query( "insert into public.profiles(id,display_name) values($1,'Account friend')", [secondAccount], ); const accountRoom = async () => ( await db.query( "select public.create_map_room($1,'normal','Forged host',4,$2,$3) as id", [dataset, secret(1), account], ) ).rows[0].id; await assert.rejects( db.query( "select public.create_map_room($1,'normal','Guest host',4,$2,null)", [dataset, secret(4)], ), /Log in to create/, ); const identityRoom = await accountRoom(); const identityHost = (await act(identityRoom, 1)).players[0]; assert.equal( identityHost.name, "Account host", "SQL ignores a forged host nickname", ); assert.equal( ( await db.query( "select account_id from public.multiplayer_players where id=$1", [identityHost.id], ) ).rows[0].account_id, account, "host linked atomically", ); await act(identityRoom, 2, "join", { name: "Guest first" }); await db.query( "select public.join_multiplayer_room($1,$2,$3,'Forged friend')", [identityRoom, secret(3), secondAccount], ); let identityView = await act(identityRoom, 3); assert.equal( identityView.players.find((p) => p.id === identityView.you).name, "Account friend", "signed-in join ignores a forged nickname", ); await act(identityRoom, 1, "leave"); identityView = await act(identityRoom, 3); assert.equal( identityView.hostId, identityView.you, "host transfer skips a guest for an account", ); await act(identityRoom, 3, "leave"); assert.equal( (await act(identityRoom, 2)).status, "closed", "guest-only party closes when host leaves", ); const guestOnly = await accountRoom(); await db.query( "select public.join_multiplayer_room($1,$2,null,'Guest custom nickname')", [guestOnly, secret(2)], ); const guestView = await act(guestOnly, 2); assert.equal( guestView.players.find((p) => p.id === guestView.you)?.name, "Guest custom nickname", ); const cleanupEvent = ( await db.query("select public.publish_discord_party($1,$2) as id", [ guestOnly, secret(1), ]) ).rows[0].id; await db.query( "update public.discord_announcements set state='sent',channel_id='123456789012345678',message_id='123456789012345679' where id=$1", [cleanupEvent], ); await act(guestOnly, 1, "leave"); assert.ok( ( await db.query( "select closed_at from public.discord_announcements where id=$1", [cleanupEvent], ) ).rows[0].closed_at, "guest-only host departure also queues Discord cleanup", ); console.log( "Multiplayer SQL account identity: forged names rejected, guest joins retained, authenticated transfer and guest-only closure verified.", ); // Subsequent match fixtures use the same authenticated account as their host. await db.query("update public.profiles set display_name='Host' where id=$1", [ account, ]); 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" }); const nextHost = (await act(capacity, 2)).you; await db.query( "insert into public.profiles(id,display_name) values($1,'Next host') on conflict(id) do nothing", [account], ); await db.query( "update public.multiplayer_players set account_id=$1 where id=$2", [account, nextHost], ); 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(); }