Some checks failed
Deploy MineGeo to Dokploy / deploy (push) Failing after 8m22s
635 lines
22 KiB
JavaScript
635 lines
22 KiB
JavaScript
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;
|
|
{
|
|
const moderationRoom = await accountRoom();
|
|
await db.query("select public.join_multiplayer_room($1,$2,$3,$4)", [
|
|
moderationRoom,
|
|
secret(2),
|
|
secondAccount,
|
|
"Ignored",
|
|
]);
|
|
await db.query("select public.join_multiplayer_room($1,$2,null,$3)", [
|
|
moderationRoom,
|
|
secret(3),
|
|
"Guest",
|
|
]);
|
|
const roster = (await act(moderationRoom, 1)).players;
|
|
let friend = roster.find((p) => p.name === "Account friend");
|
|
let guest = roster.find((p) => p.name === "Guest");
|
|
const community = async (who, action, payload = {}) =>
|
|
(
|
|
await db.query("select public.party_community($1,$2,$3,$4) as value", [
|
|
moderationRoom,
|
|
secret(who),
|
|
action,
|
|
JSON.stringify(payload),
|
|
])
|
|
).rows[0].value;
|
|
const chat = (who, text, clientId) =>
|
|
db.query("select public.multiplayer_chat($1,$2,$3,$4)", [
|
|
moderationRoom,
|
|
secret(who),
|
|
text,
|
|
clientId,
|
|
]);
|
|
await assert.rejects(
|
|
community(2, "ban", { playerId: guest.id }),
|
|
/moderator permission/,
|
|
);
|
|
await community(1, "admin", { playerId: friend.id });
|
|
await assert.rejects(
|
|
community(2, "ban", { playerId: roster[0].id }),
|
|
/owner or yourself/,
|
|
);
|
|
await community(2, "tag", { playerId: guest.id, tag: "Explorer" });
|
|
await community(2, "slow", { seconds: 10 });
|
|
const chatId = randomUUID();
|
|
await chat(3, "Hello", chatId);
|
|
await chat(3, "Hello", chatId);
|
|
await assert.rejects(chat(3, "Too fast", randomUUID()), /slow mode/);
|
|
await db.query(
|
|
"update public.party_chat_cooldowns set last_message_at=clock_timestamp()-interval '11 seconds' where room_id=$1 and access_hash=$2",
|
|
[moderationRoom, secret(3)],
|
|
);
|
|
await db.query(
|
|
"update public.multiplayer_messages set created_at=clock_timestamp()-interval '11 seconds' where room_id=$1 and player_id=$2",
|
|
[moderationRoom, guest.id],
|
|
);
|
|
await chat(3, "Hello", chatId);
|
|
await chat(3, "A new message", randomUUID());
|
|
await act(moderationRoom, 3, "leave");
|
|
await db.query("select public.join_multiplayer_room($1,$2,null,$3)", [
|
|
moderationRoom,
|
|
secret(3),
|
|
"Guest",
|
|
]);
|
|
guest = (await act(moderationRoom, 1)).players.find(
|
|
(p) => p.name === "Guest",
|
|
);
|
|
assert.ok(guest);
|
|
await assert.rejects(
|
|
chat(3, "Rejoined too fast", randomUUID()),
|
|
/slow mode/,
|
|
);
|
|
await db.query(
|
|
"update public.party_chat_cooldowns set last_message_at=clock_timestamp()-interval '11 seconds' where room_id=$1 and access_hash=$2",
|
|
[moderationRoom, secret(3)],
|
|
);
|
|
await assert.rejects(chat(3, "", randomUUID()), /Invalid chat message/);
|
|
await chat(3, "Cooldown expired", randomUUID());
|
|
const state = await community(2, "ban", { playerId: guest.id });
|
|
await assert.rejects(
|
|
db.query("select public.join_multiplayer_room($1,$2,null,$3)", [
|
|
moderationRoom,
|
|
secret(3),
|
|
"Guest",
|
|
]),
|
|
/banned/,
|
|
);
|
|
await community(2, "unban", { banId: state.bans[0].id });
|
|
await db.query("select public.join_multiplayer_room($1,$2,null,$3)", [
|
|
moderationRoom,
|
|
secret(3),
|
|
"Guest",
|
|
]);
|
|
await community(1, "unadmin", { playerId: friend.id });
|
|
await chat(2, "Signed-in player", randomUUID());
|
|
await act(moderationRoom, 2, "leave");
|
|
await db.query("select public.join_multiplayer_room($1,$2,$3,$4)", [
|
|
moderationRoom,
|
|
secret(8),
|
|
secondAccount,
|
|
"Ignored",
|
|
]);
|
|
await assert.rejects(
|
|
chat(8, "Different browser", randomUUID()),
|
|
/slow mode/,
|
|
);
|
|
await db.query(
|
|
"update public.party_chat_account_cooldowns set last_message_at=clock_timestamp()-interval '11 seconds' where room_id=$1 and account_id=$2",
|
|
[moderationRoom, secondAccount],
|
|
);
|
|
await chat(8, "Allowed after the delay", randomUUID());
|
|
friend = (await act(moderationRoom, 1)).players.find(
|
|
(p) => p.name === "Account friend",
|
|
);
|
|
assert.ok(friend);
|
|
await community(1, "ban", { playerId: friend.id });
|
|
await assert.rejects(
|
|
db.query("select public.join_multiplayer_room($1,$2,$3,$4)", [
|
|
moderationRoom,
|
|
secret(9),
|
|
secondAccount,
|
|
"Ignored",
|
|
]),
|
|
/banned/,
|
|
);
|
|
await assert.rejects(community(2, "slow", { seconds: 0 }), /membership/);
|
|
const privileges = (
|
|
await db.query(`select
|
|
has_table_privilege('anon','public.party_chat_cooldowns','select') as anon_read,
|
|
has_table_privilege('authenticated','public.party_chat_cooldowns','select') as authenticated_read,
|
|
has_table_privilege('anon','public.party_chat_account_cooldowns','select') as anon_account_read,
|
|
has_table_privilege('authenticated','public.party_chat_account_cooldowns','select') as authenticated_account_read`)
|
|
).rows[0];
|
|
assert.equal(privileges.anon_read, false);
|
|
assert.equal(privileges.authenticated_read, false);
|
|
assert.equal(privileges.anon_account_read, false);
|
|
assert.equal(privileges.authenticated_account_read, false);
|
|
await db.query("delete from public.multiplayer_rooms where id=$1", [
|
|
moderationRoom,
|
|
]);
|
|
assert.equal(
|
|
(
|
|
await db.query(
|
|
"select count(*)::integer as n from public.party_chat_cooldowns where room_id=$1",
|
|
[moderationRoom],
|
|
)
|
|
).rows[0].n,
|
|
0,
|
|
);
|
|
}
|
|
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, "Привет (: <script>alert(1)</script>", randomUUID())
|
|
).messages;
|
|
assert.equal(messages[1].name, "Friend");
|
|
assert.equal(messages[1].text, "Привет (: <script>alert(1)</script>");
|
|
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();
|
|
}
|