Add account based multiplayer hosting and bug report retention
This commit is contained in:
@@ -248,6 +248,133 @@ try {
|
||||
).rows[0];
|
||||
assert.equal(claimed.bug_finder, true);
|
||||
assert.equal(claimed.name_glow, false);
|
||||
const closeEvent = randomUUID();
|
||||
await db.query("select public.receive_bug_close($1,$2)", [
|
||||
report,
|
||||
closeEvent,
|
||||
]);
|
||||
let closed = (
|
||||
await db.query("select * from public.bug_reports where id=$1", [report])
|
||||
).rows[0];
|
||||
assert.equal(closed.state, "done");
|
||||
assert.equal(
|
||||
new Date(closed.delete_after) - new Date(closed.closed_at),
|
||||
3 * 86400000,
|
||||
);
|
||||
await db.query("select public.set_bug_report_done($1,true,null)", [report]);
|
||||
assert.equal(
|
||||
String(
|
||||
(
|
||||
await db.query("select closed_at from public.bug_reports where id=$1", [
|
||||
report,
|
||||
])
|
||||
).rows[0].closed_at,
|
||||
),
|
||||
String(closed.closed_at),
|
||||
"repeat close preserves deadline",
|
||||
);
|
||||
assert.equal(
|
||||
(
|
||||
await db.query("select public.begin_bug_purge($1,$2) as ok", [
|
||||
report,
|
||||
closed.closed_at,
|
||||
])
|
||||
).rows[0].ok,
|
||||
false,
|
||||
"cannot purge before three days",
|
||||
);
|
||||
await assert.rejects(
|
||||
db.query(
|
||||
"insert into public.bug_report_messages(report_id,kind,body,author_name) values($1,'user','new message','Guest')",
|
||||
[report],
|
||||
),
|
||||
/Done/,
|
||||
);
|
||||
await db.query("select public.set_bug_report_done($1,false,null)", [report]);
|
||||
assert.equal(
|
||||
(
|
||||
await db.query("select public.receive_bug_close($1,$2) as ok", [
|
||||
report,
|
||||
closeEvent,
|
||||
])
|
||||
).rows[0].ok,
|
||||
false,
|
||||
"replayed close command cannot reclose a reopened report",
|
||||
);
|
||||
await db.query(
|
||||
"select public.set_bug_report_done($1,true,clock_timestamp()-interval '4 days')",
|
||||
[report],
|
||||
);
|
||||
closed = (
|
||||
await db.query("select * from public.bug_reports where id=$1", [report])
|
||||
).rows[0];
|
||||
assert.equal(
|
||||
(
|
||||
await db.query("select public.begin_bug_purge($1,$2) as ok", [
|
||||
report,
|
||||
closed.closed_at,
|
||||
])
|
||||
).rows[0].ok,
|
||||
false,
|
||||
"Linear completion must be confirmed",
|
||||
);
|
||||
await db.query(
|
||||
"update public.bug_reports set linear_done_synced=true where id=$1",
|
||||
[report],
|
||||
);
|
||||
assert.equal(
|
||||
(
|
||||
await db.query("select public.begin_bug_purge($1,$2) as ok", [
|
||||
report,
|
||||
closed.closed_at,
|
||||
])
|
||||
).rows[0].ok,
|
||||
true,
|
||||
);
|
||||
assert.equal(
|
||||
(
|
||||
await db.query("select public.set_bug_report_done($1,false,null) as ok", [
|
||||
report,
|
||||
])
|
||||
).rows[0].ok,
|
||||
false,
|
||||
"cannot reopen midway through irreversible purge",
|
||||
);
|
||||
await db.query("select public.finish_bug_purge($1,$2)", [
|
||||
report,
|
||||
closed.closed_at,
|
||||
]);
|
||||
assert.equal(
|
||||
(
|
||||
await db.query(
|
||||
"select count(*)::int as n from public.bug_report_messages where report_id=$1",
|
||||
[report],
|
||||
)
|
||||
).rows[0].n,
|
||||
0,
|
||||
"messages cascade on purge",
|
||||
);
|
||||
assert.equal(
|
||||
(
|
||||
await db.query("select bug_finder from public.profiles where id=$1", [
|
||||
account,
|
||||
])
|
||||
).rows[0].bug_finder,
|
||||
true,
|
||||
"account recognition survives report deletion",
|
||||
);
|
||||
for (const role of ["anon", "authenticated"]) {
|
||||
await db.exec(`set role ${role}`);
|
||||
await assert.rejects(
|
||||
db.query("select public.set_bug_report_done($1,true,null)", [guest]),
|
||||
/permission denied/,
|
||||
);
|
||||
await assert.rejects(
|
||||
db.query("select public.finish_bug_purge($1,now())", [guest]),
|
||||
/permission denied/,
|
||||
);
|
||||
await db.exec("reset role");
|
||||
}
|
||||
console.log(
|
||||
"Bug report SQL passed: private tables, lease retries, guest claims, staff reply deduplication, grant/revoke rewards and privilege isolation.",
|
||||
);
|
||||
|
||||
146
scripts/test-bug-retention-http.mjs
Normal file
146
scripts/test-bug-retention-http.mjs
Normal file
@@ -0,0 +1,146 @@
|
||||
import assert from "node:assert/strict";
|
||||
import { randomUUID } from "node:crypto";
|
||||
import { mkdir, writeFile } from "node:fs/promises";
|
||||
import { createClient } from "@supabase/supabase-js";
|
||||
import sharp from "sharp";
|
||||
process.loadEnvFile(".env.local");
|
||||
const origin = new URL(process.argv[2] || "https://minegeo.vercel.app").origin;
|
||||
const db = createClient(
|
||||
process.env.NEXT_PUBLIC_SUPABASE_URL,
|
||||
process.env.SUPABASE_SECRET_KEY,
|
||||
{ auth: { persistSession: false, autoRefreshToken: false } },
|
||||
);
|
||||
const checked = (r) => {
|
||||
if (r.error) throw new Error(r.error.message);
|
||||
return r.data;
|
||||
};
|
||||
const cookies = new Map();
|
||||
let id;
|
||||
await mkdir(".data/retention-check", { recursive: true });
|
||||
async function api(path, body, expected = 200, stranger = false) {
|
||||
const response = await fetch(`${origin}/api/v1/bug-reports${path}`, {
|
||||
method: body === undefined ? "GET" : "POST",
|
||||
headers: {
|
||||
origin,
|
||||
"content-type": "application/json",
|
||||
cookie: stranger
|
||||
? ""
|
||||
: [...cookies].map(([k, v]) => `${k}=${v}`).join("; "),
|
||||
},
|
||||
...(body === undefined ? {} : { body: JSON.stringify(body) }),
|
||||
signal: AbortSignal.timeout(60000),
|
||||
});
|
||||
if (!stranger)
|
||||
for (const cookie of response.headers.getSetCookie()) {
|
||||
const part = cookie.split(";")[0],
|
||||
at = part.indexOf("=");
|
||||
cookies.set(part.slice(0, at), part.slice(at + 1));
|
||||
}
|
||||
const result = await response.json();
|
||||
assert.equal(response.status, expected, `${path}: ${JSON.stringify(result)}`);
|
||||
return result;
|
||||
}
|
||||
async function row() {
|
||||
return checked(
|
||||
await db.from("bug_reports").select("*").eq("id", id).maybeSingle(),
|
||||
);
|
||||
}
|
||||
async function waitFor(predicate, label) {
|
||||
for (let n = 0; n < 40; n++) {
|
||||
const current = await row();
|
||||
if (predicate(current)) return current;
|
||||
if (current?.cleanup_error) throw new Error(current.cleanup_error);
|
||||
await new Promise((r) => setTimeout(r, 3000));
|
||||
}
|
||||
throw new Error(`Timed out: ${label}`);
|
||||
}
|
||||
assert.equal((await api("")).ready, true);
|
||||
const created = await api("", {
|
||||
clientId: randomUUID(),
|
||||
title: `[Verification] Retention ${randomUUID()}`,
|
||||
description:
|
||||
"Automated temporary verification fixture for three-day report retention. This is not a real player bug.",
|
||||
displayName: "Support verification",
|
||||
pagePath: "/support",
|
||||
screenshot: true,
|
||||
});
|
||||
id = created.id;
|
||||
await writeFile(
|
||||
".data/retention-check/fixture.json",
|
||||
JSON.stringify({ id, cookies: [...cookies] }),
|
||||
);
|
||||
const image = await sharp({
|
||||
create: { width: 8, height: 8, channels: 3, background: "#334455" },
|
||||
})
|
||||
.png()
|
||||
.toBuffer();
|
||||
assert.equal(
|
||||
(
|
||||
await fetch(created.upload, {
|
||||
method: "PUT",
|
||||
headers: { "content-type": "image/png" },
|
||||
body: image,
|
||||
})
|
||||
).ok,
|
||||
true,
|
||||
);
|
||||
await api(`/${id}/submit`, {});
|
||||
await waitFor((r) => r?.issue_synced, "Linear issue creation");
|
||||
await api(`/${id}/close`, {}, 404, true);
|
||||
const closed = await api(`/${id}/close`, {});
|
||||
assert.equal(closed.state, "done");
|
||||
assert.equal(
|
||||
Date.parse(closed.delete_after) - Date.parse(closed.closed_at),
|
||||
3 * 86400000,
|
||||
);
|
||||
assert.equal((await api(`/${id}/close`, {})).delete_after, closed.delete_after);
|
||||
await api(
|
||||
`/${id}/messages`,
|
||||
{ clientId: randomUUID(), text: "Blocked after closure" },
|
||||
409,
|
||||
);
|
||||
await waitFor((r) => r?.linear_done_synced, "Linear Done status");
|
||||
console.log(
|
||||
JSON.stringify({
|
||||
fixture: id,
|
||||
checkpoint: "Done synchronized; three-day deadline and ownership verified",
|
||||
}),
|
||||
);
|
||||
// Accelerate only this journaled verification fixture; no real report is modified.
|
||||
const backdated = new Date(Date.now() - 3 * 86400000 - 60000).toISOString();
|
||||
checked(
|
||||
await db
|
||||
.from("bug_reports")
|
||||
.update({
|
||||
closed_at: backdated,
|
||||
delete_after: new Date(Date.now() - 60000).toISOString(),
|
||||
cleanup_run_id: null,
|
||||
cleanup_lease_until: null,
|
||||
status_checked_at: new Date().toISOString(),
|
||||
})
|
||||
.eq("id", id)
|
||||
.like("title", "[Verification] Retention %"),
|
||||
);
|
||||
await api(`/${id}/retry-cleanup`, {});
|
||||
await waitFor((r) => r === null, "both-system deletion");
|
||||
assert.equal(
|
||||
checked(await db.from("bug_report_messages").select("id").eq("report_id", id))
|
||||
.length,
|
||||
0,
|
||||
);
|
||||
assert.equal(
|
||||
checked(await db.storage.from("minegeo-support").list(id)).length,
|
||||
0,
|
||||
);
|
||||
await api(`/${id}`, undefined, 404);
|
||||
await writeFile(
|
||||
".data/retention-check/fixture.json",
|
||||
JSON.stringify({ id, deleted: true }),
|
||||
);
|
||||
console.log(
|
||||
JSON.stringify({
|
||||
fixture: id,
|
||||
checkpoint:
|
||||
"Expired report, messages, screenshots and Linear issue deleted",
|
||||
}),
|
||||
);
|
||||
@@ -112,18 +112,21 @@ try {
|
||||
.limit(1)
|
||||
.single(),
|
||||
);
|
||||
const host = session(),
|
||||
const host = await account("contributor"),
|
||||
friend = session(),
|
||||
outsider = session();
|
||||
const created = await host("rooms", {
|
||||
datasetId: dataset.id,
|
||||
difficulty: "normal",
|
||||
displayName: "Discord host fixture",
|
||||
playerLimit: 4,
|
||||
});
|
||||
rooms.push(created.id);
|
||||
await journal();
|
||||
const room = await host(`rooms/${created.id}`);
|
||||
assert.equal(
|
||||
room.players.find((p) => p.id === room.hostId).name,
|
||||
"Discord contributor fixture",
|
||||
);
|
||||
assert.equal(typeof room.discord.available, "boolean");
|
||||
assert.equal(room.discord.state, null);
|
||||
await outsider(`rooms/${created.id}/discord`, {}, "POST", 403);
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import assert from "node:assert/strict";
|
||||
import { randomUUID, randomBytes } from "node:crypto";
|
||||
import fs from "node:fs";
|
||||
import { chromium, expect } from "@playwright/test";
|
||||
import { createClient } from "@supabase/supabase-js";
|
||||
@@ -9,7 +10,8 @@ const db = createClient(
|
||||
process.env.SUPABASE_SECRET_KEY,
|
||||
{ auth: { persistSession: false, autoRefreshToken: false } },
|
||||
);
|
||||
const rooms = [];
|
||||
const rooms = [],
|
||||
accounts = [];
|
||||
const browser = await chromium.launch({
|
||||
headless: true,
|
||||
args: ["--use-angle=swiftshader", "--enable-unsafe-swiftshader"],
|
||||
@@ -34,6 +36,21 @@ try {
|
||||
});
|
||||
const host = await desktop.newPage(),
|
||||
friend = await mobile.newPage();
|
||||
const email = `multiplayer-browser-${randomUUID()}@example.com`,
|
||||
password = randomBytes(24).toString("base64url");
|
||||
const fixture = await db.auth.admin.createUser({
|
||||
email,
|
||||
password,
|
||||
email_confirm: true,
|
||||
user_metadata: { display_name: "Browser Host" },
|
||||
});
|
||||
if (fixture.error) throw fixture.error;
|
||||
accounts.push(fixture.data.user.id);
|
||||
const loggedIn = await desktop.request.post(`${origin}/api/v1/auth`, {
|
||||
headers: { origin },
|
||||
data: { action: "login", email, password },
|
||||
});
|
||||
assert.equal(loggedIn.status(), 200);
|
||||
const pageErrors = [];
|
||||
for (const page of [host, friend])
|
||||
page.on("pageerror", (error) => pageErrors.push(error.message));
|
||||
@@ -42,7 +59,10 @@ try {
|
||||
timeout: 60000,
|
||||
});
|
||||
await host.getByRole("button", { name: "Multiplayer", exact: true }).click();
|
||||
await host.getByLabel("Your player name").fill("Browser Host");
|
||||
await expect(host.locator(".multiplayer-settings")).toContainText(
|
||||
"Browser Host",
|
||||
);
|
||||
await expect(host.getByLabel("Your player name")).toHaveCount(0);
|
||||
await host.getByLabel("Player limit (including you)").selectOption("2");
|
||||
await host
|
||||
.getByRole("button", { name: "Create multiplayer room", exact: true })
|
||||
@@ -248,4 +268,10 @@ try {
|
||||
if (error)
|
||||
throw new Error(`Browser test-room cleanup failed: ${error.message}`);
|
||||
}
|
||||
for (const id of accounts) {
|
||||
const profile = await db.from("profiles").delete().eq("id", id);
|
||||
if (profile.error) throw profile.error;
|
||||
const account = await db.auth.admin.deleteUser(id);
|
||||
if (account.error) throw account.error;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import assert from "node:assert/strict";
|
||||
import { randomUUID } from "node:crypto";
|
||||
import { randomUUID, randomBytes } from "node:crypto";
|
||||
import { existsSync } from "node:fs";
|
||||
import { mkdir, writeFile } from "node:fs/promises";
|
||||
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;
|
||||
@@ -9,9 +10,20 @@ const db = createClient(
|
||||
process.env.SUPABASE_SECRET_KEY,
|
||||
{ auth: { persistSession: false, autoRefreshToken: false } },
|
||||
);
|
||||
const rooms = [];
|
||||
const rooms = [],
|
||||
accounts = [];
|
||||
await mkdir(".data/multiplayer-check", { recursive: true });
|
||||
const journal = () =>
|
||||
writeFile(
|
||||
".data/multiplayer-check/http-fixtures.json",
|
||||
JSON.stringify({ rooms, accounts }),
|
||||
);
|
||||
const checked = (result) => {
|
||||
if (result.error) throw new Error(result.error.message);
|
||||
return result.data;
|
||||
};
|
||||
function player() {
|
||||
let cookie;
|
||||
const cookies = new Map();
|
||||
return async (path, body, expected = 200) => {
|
||||
const response = await fetch(`${origin}/api/v1/${path}`, {
|
||||
method: body === undefined ? "GET" : "POST",
|
||||
@@ -19,35 +31,88 @@ function player() {
|
||||
...(body === undefined
|
||||
? {}
|
||||
: { origin, "content-type": "application/json" }),
|
||||
...(cookie ? { cookie } : {}),
|
||||
cookie: [...cookies]
|
||||
.map(([key, value]) => `${key}=${value}`)
|
||||
.join("; "),
|
||||
},
|
||||
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];
|
||||
for (const cookie of response.headers.getSetCookie()) {
|
||||
const part = cookie.split(";")[0],
|
||||
at = part.indexOf("=");
|
||||
cookies.set(part.slice(0, at), part.slice(at + 1));
|
||||
}
|
||||
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;
|
||||
};
|
||||
}
|
||||
async function account(displayName) {
|
||||
const email = `multiplayer-check-${randomUUID()}@example.com`,
|
||||
password = randomBytes(24).toString("base64url");
|
||||
const { user } = checked(
|
||||
await db.auth.admin.createUser({
|
||||
email,
|
||||
password,
|
||||
email_confirm: true,
|
||||
user_metadata: { display_name: "Stale metadata name" },
|
||||
}),
|
||||
);
|
||||
accounts.push(user.id);
|
||||
await journal();
|
||||
checked(
|
||||
await db
|
||||
.from("profiles")
|
||||
.insert({ id: user.id, display_name: displayName }),
|
||||
);
|
||||
const api = player();
|
||||
await api("auth", { action: "login", email, password });
|
||||
return api;
|
||||
}
|
||||
try {
|
||||
const host = player(),
|
||||
const host = await account("HTTP Host"),
|
||||
friend = player(),
|
||||
outsider = player();
|
||||
const catalog = await host("catalog");
|
||||
const datasetId = catalog.maps[0]?.id;
|
||||
assert.ok(datasetId, "A published map is required");
|
||||
await outsider(
|
||||
"rooms",
|
||||
{ datasetId, difficulty: "normal", playerLimit: 2 },
|
||||
401,
|
||||
);
|
||||
await host(
|
||||
"rooms",
|
||||
{
|
||||
datasetId,
|
||||
difficulty: "hard",
|
||||
playerLimit: 2,
|
||||
displayName: "Forged host",
|
||||
},
|
||||
400,
|
||||
);
|
||||
const created = await host("rooms", {
|
||||
datasetId,
|
||||
difficulty: "hard",
|
||||
displayName: "HTTP Host",
|
||||
playerLimit: 2,
|
||||
});
|
||||
rooms.push(created.id);
|
||||
await journal();
|
||||
const path = `rooms/${created.id}`;
|
||||
const hostView = await host(path);
|
||||
assert.equal(
|
||||
hostView.players.find((p) => p.id === hostView.hostId).name,
|
||||
"HTTP Host",
|
||||
"host uses stored profile, not stale auth metadata",
|
||||
);
|
||||
const rejoined = await host(`${path}/join`, { displayName: "Forged host" });
|
||||
assert.equal(
|
||||
rejoined.players.find((p) => p.id === rejoined.hostId).name,
|
||||
"HTTP Host",
|
||||
"host rejoin cannot rename the account",
|
||||
);
|
||||
const preview = await outsider(path);
|
||||
assert.equal(preview.you, null);
|
||||
assert.deepEqual(preview.rounds, []);
|
||||
@@ -187,14 +252,14 @@ try {
|
||||
403,
|
||||
);
|
||||
assert.equal(results.rounds.flatMap((round) => round.results).length, 10);
|
||||
const capacityHost = player();
|
||||
const capacityHost = await account("Capacity Host");
|
||||
const full = await capacityHost("rooms", {
|
||||
datasetId,
|
||||
difficulty: "normal",
|
||||
displayName: "Capacity Host",
|
||||
playerLimit: 16,
|
||||
});
|
||||
rooms.push(full.id);
|
||||
await journal();
|
||||
await Promise.all(
|
||||
Array.from({ length: 14 }, (_, i) =>
|
||||
player()(`rooms/${full.id}/join`, { displayName: `Capacity ${i + 2}` }),
|
||||
@@ -212,6 +277,11 @@ try {
|
||||
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);
|
||||
assert.equal(
|
||||
(await capacityHost(`rooms/${full.id}/leave`, {})).status,
|
||||
"closed",
|
||||
"guests cannot become party hosts",
|
||||
);
|
||||
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.",
|
||||
);
|
||||
@@ -223,5 +293,13 @@ try {
|
||||
.in("id", rooms);
|
||||
if (error)
|
||||
throw new Error(`Multiplayer test-room cleanup failed: ${error.message}`);
|
||||
rooms.length = 0;
|
||||
await journal();
|
||||
}
|
||||
for (const id of accounts) {
|
||||
checked(await db.from("profiles").delete().eq("id", id));
|
||||
checked(await db.auth.admin.deleteUser(id));
|
||||
}
|
||||
accounts.length = 0;
|
||||
await journal();
|
||||
}
|
||||
|
||||
@@ -65,6 +65,107 @@ try {
|
||||
])
|
||||
).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.",
|
||||
);
|
||||
for (const cap of [1, 17, null])
|
||||
await assert.rejects(create(cap), /Choose between 2 and 16 players/);
|
||||
const room = await create();
|
||||
@@ -255,6 +356,15 @@ try {
|
||||
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()),
|
||||
|
||||
@@ -231,7 +231,6 @@ try {
|
||||
await api(player.context, "rooms", {
|
||||
datasetId: map.id,
|
||||
difficulty: "easy",
|
||||
displayName: "Temporary host alias",
|
||||
playerLimit: 3,
|
||||
})
|
||||
).id;
|
||||
|
||||
Reference in New Issue
Block a user