Add private chat to multiplayer rooms
This commit is contained in:
@@ -1,4 +1,5 @@
|
||||
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";
|
||||
|
||||
@@ -83,6 +84,46 @@ try {
|
||||
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,
|
||||
@@ -103,6 +144,11 @@ try {
|
||||
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,
|
||||
@@ -185,6 +231,11 @@ try {
|
||||
} 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);
|
||||
@@ -201,9 +252,14 @@ try {
|
||||
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,
|
||||
@@ -257,8 +313,35 @@ try {
|
||||
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 [
|
||||
@@ -266,17 +349,19 @@ try {
|
||||
"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: 16-player cap, owner permissions, private answers, synchronized rounds, deadlines, retries, ties, departure, and client-role isolation.",
|
||||
"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();
|
||||
|
||||
Reference in New Issue
Block a user