Add player bug reports and profile identity features
This commit is contained in:
286
scripts/test-bug-reports-browser.mjs
Normal file
286
scripts/test-bug-reports-browser.mjs
Normal file
@@ -0,0 +1,286 @@
|
||||
import assert from "node:assert/strict";
|
||||
import { randomUUID, randomBytes } from "node:crypto";
|
||||
import { mkdir, writeFile } from "node:fs/promises";
|
||||
import { chromium } from "@playwright/test";
|
||||
import { createClient } from "@supabase/supabase-js";
|
||||
import sharp from "sharp";
|
||||
|
||||
process.loadEnvFile(".env.local");
|
||||
const origin = process.argv[2] || "https://minegeo.vercel.app";
|
||||
const db = createClient(
|
||||
process.env.NEXT_PUBLIC_SUPABASE_URL,
|
||||
process.env.SUPABASE_SECRET_KEY,
|
||||
{ auth: { persistSession: false, autoRefreshToken: false } },
|
||||
);
|
||||
const browser = await chromium.launch({ headless: true });
|
||||
const context = await browser.newContext({ locale: "en-US" });
|
||||
const page = await context.newPage();
|
||||
page.setDefaultTimeout(30000);
|
||||
let reportId, accountId;
|
||||
const checked = (result) => {
|
||||
if (result.error) throw new Error(result.error.message);
|
||||
return result.data;
|
||||
};
|
||||
const statePath = ".data/support-deploy/fixture.json";
|
||||
async function phase(value) {
|
||||
await writeFile(statePath, JSON.stringify({ reportId, phase: value }));
|
||||
console.log("Support verification: " + value);
|
||||
}
|
||||
async function until(check, label, timeout = 120000) {
|
||||
const end = Date.now() + timeout;
|
||||
while (Date.now() < end) {
|
||||
if (await check()) return;
|
||||
await new Promise((resolve) => setTimeout(resolve, 2000));
|
||||
}
|
||||
throw new Error("Timed out: " + label);
|
||||
}
|
||||
async function json(path, body) {
|
||||
const response =
|
||||
body === undefined
|
||||
? await context.request.get(origin + "/api/v1/" + path)
|
||||
: await context.request.post(origin + "/api/v1/" + path, {
|
||||
headers: { origin },
|
||||
data: body,
|
||||
});
|
||||
assert.equal(response.status(), 200, await response.text());
|
||||
return response.json();
|
||||
}
|
||||
try {
|
||||
await mkdir(".data/support-deploy", { recursive: true });
|
||||
await page.goto(origin, { waitUntil: "networkidle" });
|
||||
assert.equal(await page.locator(".header-actions > a").count(), 2);
|
||||
await page.getByRole("link", { name: "Report a bug", exact: true }).click();
|
||||
await page.getByLabel("Your player name").fill("Support verification");
|
||||
await page
|
||||
.getByLabel("Bug title", { exact: true })
|
||||
.fill("[Verification] Player support " + randomUUID().slice(0, 8));
|
||||
await page
|
||||
.getByLabel("What happened?", { exact: true })
|
||||
.fill(
|
||||
"Temporary automated verification of the private support conversation and screenshot.",
|
||||
);
|
||||
const image = await sharp({
|
||||
create: { width: 180, height: 100, channels: 3, background: "#8dce66" },
|
||||
})
|
||||
.png()
|
||||
.toBuffer();
|
||||
await page
|
||||
.getByLabel("Screenshot (optional)")
|
||||
.setInputFiles({
|
||||
name: "support-test.png",
|
||||
mimeType: "image/png",
|
||||
buffer: image,
|
||||
});
|
||||
await page
|
||||
.getByRole("button", { name: "Send bug report", exact: true })
|
||||
.click();
|
||||
await page.waitForURL(/\/support\/[0-9a-f-]{36}$/, { timeout: 60000 });
|
||||
reportId = page.url().split("/").pop();
|
||||
await phase("submitted");
|
||||
await until(async () => {
|
||||
const r = await json("bug-reports/" + reportId);
|
||||
return r.state === "open" && r.messages[0]?.delivered;
|
||||
}, "Linear issue creation");
|
||||
const report = checked(
|
||||
await db.from("bug_reports").select("*").eq("id", reportId).single(),
|
||||
);
|
||||
assert.ok(report.linear_identifier);
|
||||
assert.match(report.linear_asset_url, /^https:/);
|
||||
assert.ok(report.screenshot_path.endsWith("/screenshot.webp"));
|
||||
assert.notEqual(report.client_id, report.id);
|
||||
const retry = await json("bug-reports", {
|
||||
clientId: report.client_id,
|
||||
title: report.title,
|
||||
description: report.description,
|
||||
displayName: report.display_name,
|
||||
pagePath: report.page_path,
|
||||
screenshot: true,
|
||||
});
|
||||
assert.equal(retry.id, reportId);
|
||||
assert.equal(retry.upload, null);
|
||||
await Promise.all([
|
||||
json("bug-reports/" + reportId + "/submit", {}),
|
||||
json("bug-reports/" + reportId + "/submit", {}),
|
||||
]);
|
||||
const stranger = await browser.newContext();
|
||||
assert.equal(
|
||||
(
|
||||
await stranger.request.get(origin + "/api/v1/bug-reports/" + reportId)
|
||||
).status(),
|
||||
404,
|
||||
);
|
||||
assert.equal(
|
||||
(
|
||||
await stranger.request.post(origin + "/api/v1/bug-reports", {
|
||||
headers: { origin: "https://foreign.example" },
|
||||
data: {},
|
||||
})
|
||||
).status(),
|
||||
403,
|
||||
);
|
||||
await stranger.close();
|
||||
assert.equal(
|
||||
(
|
||||
await context.request.post(origin + "/api/support/linear", {
|
||||
data: { type: "Comment" },
|
||||
headers: { "linear-signature": "0".repeat(64) },
|
||||
})
|
||||
).status(),
|
||||
401,
|
||||
);
|
||||
const anonymous = createClient(
|
||||
process.env.NEXT_PUBLIC_SUPABASE_URL,
|
||||
process.env.NEXT_PUBLIC_SUPABASE_PUBLISHABLE_KEY,
|
||||
{ auth: { persistSession: false } },
|
||||
);
|
||||
assert.ok((await anonymous.from("bug_reports").select("id")).error);
|
||||
const text = "Browser reply verification.\n#player-report-glow grant";
|
||||
await page.getByLabel("Message support", { exact: true }).fill(text);
|
||||
await page.getByRole("button", { name: "Send message", exact: true }).click();
|
||||
await until(async () => {
|
||||
const r = await json("bug-reports/" + reportId);
|
||||
return r.messages.some((m) => m.body === text && m.delivered);
|
||||
}, "player comment delivery");
|
||||
const sent = checked(
|
||||
await db
|
||||
.from("bug_report_messages")
|
||||
.select("client_id")
|
||||
.eq("report_id", reportId)
|
||||
.eq("body", text)
|
||||
.single(),
|
||||
);
|
||||
await json("bug-reports/" + reportId + "/messages", {
|
||||
clientId: sent.client_id,
|
||||
text,
|
||||
});
|
||||
assert.equal(
|
||||
checked(
|
||||
await db
|
||||
.from("bug_report_messages")
|
||||
.select("id")
|
||||
.eq("report_id", reportId)
|
||||
.eq("body", text),
|
||||
).length,
|
||||
1,
|
||||
);
|
||||
const password = randomBytes(24).toString("base64url"),
|
||||
email = "support-verify-" + randomUUID() + "@example.com";
|
||||
const account = checked(
|
||||
await db.auth.admin.createUser({
|
||||
email,
|
||||
password,
|
||||
email_confirm: true,
|
||||
user_metadata: { display_name: "Support verification" },
|
||||
}),
|
||||
);
|
||||
accountId = account.user.id;
|
||||
await json("auth", {
|
||||
action: "login",
|
||||
email,
|
||||
password,
|
||||
next: "/support/" + reportId,
|
||||
});
|
||||
await page.reload({ waitUntil: "networkidle" });
|
||||
await page
|
||||
.getByRole("button", { name: "Keep this report in my account" })
|
||||
.click();
|
||||
await until(
|
||||
async () =>
|
||||
checked(
|
||||
await db
|
||||
.from("bug_reports")
|
||||
.select("owner_id")
|
||||
.eq("id", reportId)
|
||||
.single(),
|
||||
).owner_id === accountId,
|
||||
"guest report claim",
|
||||
);
|
||||
await phase("ready-for-staff");
|
||||
await until(
|
||||
async () => {
|
||||
const r = await json("bug-reports/" + reportId);
|
||||
return (
|
||||
r.glow_requested &&
|
||||
r.messages.some(
|
||||
(m) => m.kind === "staff" && m.body === "Verified support reply.",
|
||||
)
|
||||
);
|
||||
},
|
||||
"staff reply and glow",
|
||||
240000,
|
||||
);
|
||||
assert.equal(
|
||||
checked(
|
||||
await db
|
||||
.from("profiles")
|
||||
.select("name_glow")
|
||||
.eq("id", accountId)
|
||||
.single(),
|
||||
).name_glow,
|
||||
true,
|
||||
);
|
||||
await page.goto(origin + "/settings", { waitUntil: "networkidle" });
|
||||
await page
|
||||
.locator(".username-glow")
|
||||
.filter({ hasText: "Support verification" })
|
||||
.waitFor();
|
||||
await page.goto(origin + "/support/" + reportId, {
|
||||
waitUntil: "networkidle",
|
||||
});
|
||||
await page.getByText("Verified support reply.", { exact: true }).waitFor();
|
||||
await page.setViewportSize({ width: 390, height: 844 });
|
||||
assert.ok(
|
||||
await page.evaluate(
|
||||
() => document.documentElement.scrollWidth <= innerWidth + 1,
|
||||
),
|
||||
"mobile overflow",
|
||||
);
|
||||
await page.screenshot({
|
||||
path: ".data/support-deploy/conversation-mobile.png",
|
||||
fullPage: true,
|
||||
});
|
||||
const view = await json("bug-reports/" + reportId);
|
||||
assert.equal(
|
||||
view.messages.filter((m) => m.kind === "staff").length,
|
||||
1,
|
||||
"internal comments stay internal",
|
||||
);
|
||||
await phase("ready-for-revoke");
|
||||
await until(
|
||||
async () =>
|
||||
!checked(
|
||||
await db
|
||||
.from("profiles")
|
||||
.select("name_glow")
|
||||
.eq("id", accountId)
|
||||
.single(),
|
||||
).name_glow,
|
||||
"reward revoke",
|
||||
240000,
|
||||
);
|
||||
await page.goto(origin + "/settings", { waitUntil: "networkidle" });
|
||||
assert.equal(await page.locator(".username-glow").count(), 0);
|
||||
await phase("passed");
|
||||
console.log(
|
||||
"Production support passed: screenshot, one issue, retries, private access, player reply, account linking, staff reply, real glow grant/revoke, mobile UI.",
|
||||
);
|
||||
} catch (error) {
|
||||
console.error(error.message);
|
||||
process.exitCode = 1;
|
||||
await phase("failed");
|
||||
} finally {
|
||||
// Only records created by this verification are removed.
|
||||
if (reportId) {
|
||||
checked(
|
||||
await db.storage
|
||||
.from("minegeo-support")
|
||||
.remove([reportId + "/input", reportId + "/screenshot.webp"]),
|
||||
);
|
||||
checked(await db.from("bug_reports").delete().eq("id", reportId));
|
||||
}
|
||||
if (accountId) {
|
||||
checked(await db.from("profiles").delete().eq("id", accountId));
|
||||
checked(await db.auth.admin.deleteUser(accountId));
|
||||
}
|
||||
await browser.close();
|
||||
}
|
||||
259
scripts/test-bug-reports-sql.mjs
Normal file
259
scripts/test-bug-reports-sql.mjs
Normal file
@@ -0,0 +1,259 @@
|
||||
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";
|
||||
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 = randomUUID();
|
||||
await db.query("insert into auth.users values($1)", [account]);
|
||||
await db.query(
|
||||
"insert into public.profiles(id,display_name) values($1,'Support fixture')",
|
||||
[account],
|
||||
);
|
||||
const hash = "a".repeat(64),
|
||||
nonce = randomUUID();
|
||||
const report = (
|
||||
await db.query(
|
||||
"insert into public.bug_reports(client_id,access_hash,display_name,title,description,state) values($1,$2,'Guest','Test issue','A valid test bug description.','pending') returning id",
|
||||
[nonce, hash],
|
||||
)
|
||||
).rows[0].id;
|
||||
assert.notEqual(
|
||||
report,
|
||||
nonce,
|
||||
"Linear issue IDs are server generated, not chosen by the player",
|
||||
);
|
||||
const rpc = async (sql, params) => (await db.query(sql, params)).rows[0];
|
||||
assert.equal(
|
||||
(await rpc("select public.claim_bug_sync($1,'worker-a') as ok", [report]))
|
||||
.ok,
|
||||
true,
|
||||
);
|
||||
assert.equal(
|
||||
(await rpc("select public.claim_bug_sync($1,'worker-b') as ok", [report]))
|
||||
.ok,
|
||||
false,
|
||||
);
|
||||
await db.query(
|
||||
"update public.bug_reports set lease_until=now()-interval '1 second' where id=$1",
|
||||
[report],
|
||||
);
|
||||
assert.equal(
|
||||
(await rpc("select public.claim_bug_sync($1,'worker-b') as ok", [report]))
|
||||
.ok,
|
||||
true,
|
||||
);
|
||||
const command = randomUUID();
|
||||
await db.query("select public.receive_bug_reply($1,$2,'Staff','',true)", [
|
||||
report,
|
||||
command,
|
||||
]);
|
||||
assert.equal(
|
||||
(
|
||||
await db.query(
|
||||
"select glow_requested from public.bug_reports where id=$1",
|
||||
[report],
|
||||
)
|
||||
).rows[0].glow_requested,
|
||||
true,
|
||||
);
|
||||
await assert.rejects(
|
||||
db.query("select public.claim_bug_report($1,$2,$3)", [
|
||||
report,
|
||||
"b".repeat(64),
|
||||
account,
|
||||
]),
|
||||
/Report not found/,
|
||||
);
|
||||
await db.query("select public.claim_bug_report($1,$2,$3)", [
|
||||
report,
|
||||
hash,
|
||||
account,
|
||||
]);
|
||||
assert.equal(
|
||||
(
|
||||
await db.query("select name_glow from public.profiles where id=$1", [
|
||||
account,
|
||||
])
|
||||
).rows[0].name_glow,
|
||||
true,
|
||||
);
|
||||
await db.query("select public.receive_bug_reply($1,$2,'Staff','',false)", [
|
||||
report,
|
||||
randomUUID(),
|
||||
]);
|
||||
await db.query("select public.receive_bug_reply($1,$2,'Staff','',true)", [
|
||||
report,
|
||||
command,
|
||||
]);
|
||||
assert.equal(
|
||||
(
|
||||
await db.query("select name_glow from public.profiles where id=$1", [
|
||||
account,
|
||||
])
|
||||
).rows[0].name_glow,
|
||||
false,
|
||||
"old duplicate grant cannot override a revoke",
|
||||
);
|
||||
await db.query(
|
||||
"select public.receive_bug_reply($1,$2,'Staff','We found the bug.',null)",
|
||||
[report, randomUUID()],
|
||||
);
|
||||
assert.equal(
|
||||
(
|
||||
await db.query(
|
||||
"select count(*)::integer as n from public.bug_report_messages where report_id=$1",
|
||||
[report],
|
||||
)
|
||||
).rows[0].n,
|
||||
3,
|
||||
);
|
||||
for (const role of ["anon", "authenticated"]) {
|
||||
await db.exec(`set role ${role}`);
|
||||
for (const table of ["bug_reports", "bug_report_messages"])
|
||||
await assert.rejects(
|
||||
db.query(`select * from public.${table}`),
|
||||
/permission denied/,
|
||||
);
|
||||
await assert.rejects(
|
||||
db.query("select public.receive_bug_reply($1,$2,'Spoof','Fake',true)", [
|
||||
report,
|
||||
randomUUID(),
|
||||
]),
|
||||
/permission denied/,
|
||||
);
|
||||
await assert.rejects(
|
||||
db.query("select public.claim_bug_report($1,$2,$3)", [
|
||||
report,
|
||||
hash,
|
||||
account,
|
||||
]),
|
||||
/permission denied/,
|
||||
);
|
||||
await db.exec("reset role");
|
||||
}
|
||||
await db.exec(
|
||||
"create role reward_probe bypassrls;grant update on public.profiles to reward_probe;grant select on public.profiles to reward_probe;set role reward_probe;",
|
||||
);
|
||||
await assert.rejects(
|
||||
db.query("update public.profiles set name_glow=true where id=$1", [
|
||||
account,
|
||||
]),
|
||||
/Reward permission/,
|
||||
);
|
||||
await db.exec("reset role");
|
||||
assert.equal(
|
||||
(
|
||||
await db.query("select bug_finder from public.profiles where id=$1", [
|
||||
account,
|
||||
])
|
||||
).rows[0].bug_finder,
|
||||
true,
|
||||
"revoking glow keeps the confirmed bug-finder badge",
|
||||
);
|
||||
await assert.rejects(
|
||||
db.query("update public.profiles set glow_color='#123456' where id=$1", [
|
||||
account,
|
||||
]),
|
||||
/Earn a username glow/,
|
||||
);
|
||||
const mod = randomUUID();
|
||||
await db.query("insert into auth.users values($1)", [mod]);
|
||||
await db.query(
|
||||
"insert into public.profiles(id,role) values($1,'moderator')",
|
||||
[mod],
|
||||
);
|
||||
await assert.rejects(
|
||||
db.query("select public.set_user_prefix($1,$2,'Builder','gem')", [
|
||||
account,
|
||||
mod,
|
||||
]),
|
||||
/Moderator permission/,
|
||||
);
|
||||
await db.query("select public.set_user_prefix($1,$2,'Builder','gem')", [
|
||||
mod,
|
||||
account,
|
||||
]);
|
||||
assert.equal(
|
||||
(
|
||||
await db.query("select custom_prefix from public.profiles where id=$1", [
|
||||
account,
|
||||
])
|
||||
).rows[0].custom_prefix,
|
||||
"Builder",
|
||||
);
|
||||
await db.query("select public.set_user_prefix($1,$2,null,'star')", [
|
||||
mod,
|
||||
account,
|
||||
]);
|
||||
assert.equal(
|
||||
(
|
||||
await db.query("select custom_prefix from public.profiles where id=$1", [
|
||||
account,
|
||||
])
|
||||
).rows[0].custom_prefix,
|
||||
null,
|
||||
);
|
||||
await db.exec("set role reward_probe");
|
||||
for (const column of [
|
||||
"role='moderator'",
|
||||
"bug_finder=false",
|
||||
"custom_prefix='Spoof'",
|
||||
"custom_prefix_icon='crown'",
|
||||
])
|
||||
await assert.rejects(
|
||||
db.query(`update public.profiles set ${column} where id=$1`, [account]),
|
||||
/Moderator permission/,
|
||||
);
|
||||
await assert.rejects(
|
||||
db.query("select public.set_user_prefix($1,$2,'Spoof','star')", [
|
||||
mod,
|
||||
account,
|
||||
]),
|
||||
/permission denied/,
|
||||
);
|
||||
await db.exec("reset role");
|
||||
const guest = (
|
||||
await db.query(
|
||||
"insert into public.bug_reports(client_id,access_hash,display_name,title,description,state) values($1,$2,'Guest','Guest verification','A guest verification description.','pending') returning id",
|
||||
[randomUUID(), hash],
|
||||
)
|
||||
).rows[0].id;
|
||||
await db.query("select public.receive_bug_reply($1,$2,'Staff','',true)", [
|
||||
guest,
|
||||
randomUUID(),
|
||||
]);
|
||||
await db.query("select public.receive_bug_reply($1,$2,'Staff','',false)", [
|
||||
guest,
|
||||
randomUUID(),
|
||||
]);
|
||||
await db.query("select public.claim_bug_report($1,$2,$3)", [
|
||||
guest,
|
||||
hash,
|
||||
mod,
|
||||
]);
|
||||
const claimed = (
|
||||
await db.query(
|
||||
"select bug_finder,name_glow from public.profiles where id=$1",
|
||||
[mod],
|
||||
)
|
||||
).rows[0];
|
||||
assert.equal(claimed.bug_finder, true);
|
||||
assert.equal(claimed.name_glow, false);
|
||||
console.log(
|
||||
"Bug report SQL passed: private tables, lease retries, guest claims, staff reply deduplication, grant/revoke rewards and privilege isolation.",
|
||||
);
|
||||
} catch (error) {
|
||||
console.error(error.message);
|
||||
process.exitCode = 1;
|
||||
} finally {
|
||||
await db.close();
|
||||
}
|
||||
382
scripts/test-profile-appearance-browser.mjs
Normal file
382
scripts/test-profile-appearance-browser.mjs
Normal file
@@ -0,0 +1,382 @@
|
||||
import assert from "node:assert/strict";
|
||||
import { randomUUID, randomBytes } from "node:crypto";
|
||||
import { mkdir, writeFile } from "node:fs/promises";
|
||||
import { chromium } from "@playwright/test";
|
||||
import { createClient } from "@supabase/supabase-js";
|
||||
|
||||
process.loadEnvFile(".env.local");
|
||||
const origin = process.argv[2] || "https://minegeo.vercel.app",
|
||||
reportId = process.argv[3];
|
||||
assert.match(
|
||||
reportId ?? "",
|
||||
/^[0-9a-f-]{36}$/,
|
||||
"Pass a disposable Linear test issue UUID for webhook verification",
|
||||
);
|
||||
const db = createClient(
|
||||
process.env.NEXT_PUBLIC_SUPABASE_URL,
|
||||
process.env.SUPABASE_SECRET_KEY,
|
||||
{ auth: { persistSession: false, autoRefreshToken: false } },
|
||||
);
|
||||
const browser = await chromium.launch({ headless: true }),
|
||||
accounts = [],
|
||||
contexts = [];
|
||||
let roomId,
|
||||
createdReport = false;
|
||||
const checked = (result) => {
|
||||
if (result.error) throw new Error(result.error.message);
|
||||
return result.data;
|
||||
};
|
||||
const phase = async (value) => {
|
||||
await writeFile(
|
||||
".data/support-deploy/appearance-fixture.json",
|
||||
JSON.stringify({ reportId, phase: value }),
|
||||
);
|
||||
console.log("Appearance verification: " + value);
|
||||
};
|
||||
async function until(check, label, timeout = 120000) {
|
||||
const end = Date.now() + timeout;
|
||||
while (Date.now() < end) {
|
||||
if (await check()) return;
|
||||
await new Promise((resolve) => setTimeout(resolve, 1500));
|
||||
}
|
||||
throw new Error("Timed out: " + label);
|
||||
}
|
||||
async function api(
|
||||
context,
|
||||
path,
|
||||
body,
|
||||
method = body === undefined ? "GET" : "POST",
|
||||
status = 200,
|
||||
) {
|
||||
const response = await context.request.fetch(origin + "/api/v1/" + path, {
|
||||
method,
|
||||
headers: { origin },
|
||||
...(body === undefined ? {} : { data: body }),
|
||||
});
|
||||
assert.equal(
|
||||
response.status(),
|
||||
status,
|
||||
path + ": " + (await response.text()),
|
||||
);
|
||||
return response.json();
|
||||
}
|
||||
async function account(role) {
|
||||
const email = "appearance-" + randomUUID() + "@example.com",
|
||||
password = randomBytes(24).toString("base64url");
|
||||
const result = checked(
|
||||
await db.auth.admin.createUser({
|
||||
email,
|
||||
password,
|
||||
email_confirm: true,
|
||||
user_metadata: {
|
||||
display_name:
|
||||
role === "moderator" ? "Prefix moderator test" : "Badge player test",
|
||||
},
|
||||
}),
|
||||
);
|
||||
const id = result.user.id;
|
||||
accounts.push(id);
|
||||
checked(
|
||||
await db.from("profiles").insert({
|
||||
id,
|
||||
role,
|
||||
display_name:
|
||||
role === "moderator" ? "Prefix moderator test" : "Badge player test",
|
||||
}),
|
||||
);
|
||||
const context = await browser.newContext({ locale: "en-US" });
|
||||
contexts.push(context);
|
||||
await api(context, "auth", {
|
||||
action: "login",
|
||||
email,
|
||||
password,
|
||||
next: "/settings",
|
||||
});
|
||||
return { id, context, page: await context.newPage() };
|
||||
}
|
||||
try {
|
||||
await mkdir(".data/support-deploy", { recursive: true });
|
||||
await phase("starting");
|
||||
const player = await account("contributor"),
|
||||
mod = await account("moderator");
|
||||
await api(player.context, "profile", { glowColor: "#54baff" }, "PATCH", 403);
|
||||
await api(
|
||||
player.context,
|
||||
"profile",
|
||||
{ custom_prefix: "Spoof" },
|
||||
"PATCH",
|
||||
400,
|
||||
);
|
||||
await api(player.context, "moderation/users", undefined, "GET", 403);
|
||||
await api(
|
||||
player.context,
|
||||
"moderation/users/" + player.id,
|
||||
{ prefix: "Spoof", icon: "star" },
|
||||
"PATCH",
|
||||
403,
|
||||
);
|
||||
await player.page.goto(origin + "/settings", { waitUntil: "networkidle" });
|
||||
assert.equal(await player.page.locator("input[type=color]").count(), 0);
|
||||
await mod.page.goto(origin + "/settings", { waitUntil: "networkidle" });
|
||||
assert.ok((await mod.page.locator(".username-moderator svg").count()) > 0);
|
||||
await mod.page.goto(origin + "/moderation", { waitUntil: "networkidle" });
|
||||
await mod.page
|
||||
.getByLabel("Player name or account ID", { exact: true })
|
||||
.fill(player.id);
|
||||
await mod.page
|
||||
.getByRole("button", { name: "Find players", exact: true })
|
||||
.click();
|
||||
await mod.page
|
||||
.locator(".prefix-player-list button")
|
||||
.filter({ hasText: player.id })
|
||||
.click();
|
||||
await mod.page
|
||||
.getByLabel("Custom prefix", { exact: true })
|
||||
.fill("Cartographer");
|
||||
await mod.page.getByLabel("Prefix icon", { exact: true }).selectOption("gem");
|
||||
await mod.page
|
||||
.getByRole("button", { name: "Save prefix", exact: true })
|
||||
.click();
|
||||
await until(
|
||||
async () => {
|
||||
const saved = await mod.page
|
||||
.getByText("Player prefix saved.", { exact: true })
|
||||
.count();
|
||||
const alerts = await mod.page.locator("[role=alert]").allTextContents();
|
||||
const visibleErrors = alerts.filter((message) => message.trim());
|
||||
if (visibleErrors.length)
|
||||
throw new Error("Prefix save: " + visibleErrors.join(" | "));
|
||||
return saved > 0;
|
||||
},
|
||||
"prefix save",
|
||||
30000,
|
||||
);
|
||||
await player.page.reload({ waitUntil: "networkidle" });
|
||||
assert.ok(
|
||||
(await player.page
|
||||
.locator(".username-custom-prefix")
|
||||
.filter({ hasText: "Cartographer" })
|
||||
.count()) > 0,
|
||||
);
|
||||
const current = checked(
|
||||
await db.from("profiles").select("*").eq("id", player.id).single(),
|
||||
);
|
||||
assert.equal(current.role, "contributor");
|
||||
assert.equal(current.name_glow, false);
|
||||
assert.equal(current.bug_finder, false);
|
||||
checked(
|
||||
await db.from("bug_reports").insert({
|
||||
id: reportId,
|
||||
client_id: randomUUID(),
|
||||
owner_id: player.id,
|
||||
access_hash: randomBytes(32).toString("hex"),
|
||||
display_name: "Badge player test",
|
||||
title: "Appearance verification",
|
||||
description:
|
||||
"Temporary test report for real webhook badge and glow verification.",
|
||||
state: "open",
|
||||
issue_synced: true,
|
||||
linear_identifier: "Verification",
|
||||
}),
|
||||
);
|
||||
createdReport = true;
|
||||
await phase("ready-for-grant");
|
||||
await until(
|
||||
async () => {
|
||||
const row = checked(
|
||||
await db
|
||||
.from("profiles")
|
||||
.select("name_glow,bug_finder")
|
||||
.eq("id", player.id)
|
||||
.single(),
|
||||
);
|
||||
return row.name_glow && row.bug_finder;
|
||||
},
|
||||
"real webhook bug finder grant",
|
||||
240000,
|
||||
);
|
||||
await player.page.reload({ waitUntil: "networkidle" });
|
||||
await player.page.locator("input[type=color]").waitFor();
|
||||
assert.ok(
|
||||
(await player.page.locator(".username-bug-finder svg").count()) > 0,
|
||||
);
|
||||
await player.page.locator("input[type=color]").evaluate((input) => {
|
||||
Object.getOwnPropertyDescriptor(
|
||||
HTMLInputElement.prototype,
|
||||
"value",
|
||||
).set.call(input, "#54baff");
|
||||
input.dispatchEvent(new Event("input", { bubbles: true }));
|
||||
input.dispatchEvent(new Event("change", { bubbles: true }));
|
||||
});
|
||||
await player.page
|
||||
.getByRole("button", { name: "Save glow color", exact: true })
|
||||
.click();
|
||||
await player.page.getByText("Glow color saved.", { exact: true }).waitFor();
|
||||
await player.page.reload({ waitUntil: "networkidle" });
|
||||
assert.equal(
|
||||
await player.page.locator("input[type=color]").inputValue(),
|
||||
"#54baff",
|
||||
);
|
||||
assert.equal(
|
||||
await player.page
|
||||
.locator(".account-page-heading .username-glow")
|
||||
.evaluate((element) => getComputedStyle(element).color),
|
||||
"rgb(84, 186, 255)",
|
||||
);
|
||||
const map = checked(
|
||||
await db.from("datasets").select("id").eq("state", "published").limit(1),
|
||||
)[0];
|
||||
assert.ok(map);
|
||||
roomId = (
|
||||
await api(player.context, "rooms", {
|
||||
datasetId: map.id,
|
||||
difficulty: "easy",
|
||||
displayName: "Temporary host alias",
|
||||
playerLimit: 3,
|
||||
})
|
||||
).id;
|
||||
const guest = await browser.newContext({ locale: "en-US" });
|
||||
contexts.push(guest);
|
||||
await api(guest, "rooms/" + roomId + "/join", {
|
||||
displayName: "Badge player test",
|
||||
});
|
||||
await api(mod.context, "rooms/" + roomId + "/join", {
|
||||
displayName: "Temporary mod alias",
|
||||
});
|
||||
const room = await api(player.context, "rooms/" + roomId);
|
||||
const host = room.players.find((p) => p.id === room.hostId);
|
||||
assert.equal(host.name, "Badge player test");
|
||||
assert.equal(host.appearance.glow_color, "#54baff");
|
||||
assert.equal(host.appearance.bug_finder, true);
|
||||
assert.equal(host.appearance.custom_prefix, "Cartographer");
|
||||
const guestView = await api(guest, "rooms/" + roomId),
|
||||
guestPlayer = guestView.players.find((p) => p.id === guestView.you);
|
||||
assert.equal(
|
||||
guestPlayer.appearance,
|
||||
undefined,
|
||||
"copying a name cannot copy a badge",
|
||||
);
|
||||
await api(player.context, "rooms/" + roomId + "/chat", {
|
||||
clientId: randomUUID(),
|
||||
text: "Verified colored badge in chat.",
|
||||
});
|
||||
const chat = await api(guest, "rooms/" + roomId + "/chat");
|
||||
assert.equal(chat.messages[0].appearance.bug_finder, true);
|
||||
assert.equal(chat.messages[0].appearance.custom_prefix, "Cartographer");
|
||||
await player.page.goto(origin + "/multiplayer/" + roomId, {
|
||||
waitUntil: "networkidle",
|
||||
});
|
||||
await player.page.getByRole("button", { name: /^Chat/ }).click();
|
||||
await player.page
|
||||
.getByText("Verified colored badge in chat.", { exact: true })
|
||||
.waitFor();
|
||||
assert.ok(
|
||||
(await player.page
|
||||
.locator(".room-chat-log .username-bug-finder svg")
|
||||
.count()) > 0,
|
||||
);
|
||||
await player.page.setViewportSize({ width: 390, height: 844 });
|
||||
assert.ok(
|
||||
await player.page.evaluate(
|
||||
() => document.documentElement.scrollWidth <= innerWidth + 1,
|
||||
),
|
||||
"mobile roster overflow",
|
||||
);
|
||||
await player.page.screenshot({
|
||||
path: ".data/support-deploy/appearance-multiplayer-mobile.png",
|
||||
fullPage: true,
|
||||
});
|
||||
await player.page.goto(origin + "/settings", { waitUntil: "networkidle" });
|
||||
assert.ok(
|
||||
await player.page.evaluate(
|
||||
() => document.documentElement.scrollWidth <= innerWidth + 1,
|
||||
),
|
||||
"mobile settings overflow",
|
||||
);
|
||||
await player.page.screenshot({
|
||||
path: ".data/support-deploy/appearance-settings-mobile.png",
|
||||
fullPage: true,
|
||||
});
|
||||
await mod.page
|
||||
.getByRole("button", { name: "Remove prefix", exact: true })
|
||||
.click();
|
||||
await until(
|
||||
async () =>
|
||||
checked(
|
||||
await db
|
||||
.from("profiles")
|
||||
.select("custom_prefix")
|
||||
.eq("id", player.id)
|
||||
.single(),
|
||||
).custom_prefix === null,
|
||||
"prefix removal",
|
||||
);
|
||||
await player.page.reload({ waitUntil: "networkidle" });
|
||||
assert.equal(await player.page.locator(".username-custom-prefix").count(), 0);
|
||||
assert.ok((await player.page.locator(".username-bug-finder").count()) > 0);
|
||||
await phase("ready-for-revoke");
|
||||
await until(
|
||||
async () =>
|
||||
!checked(
|
||||
await db
|
||||
.from("profiles")
|
||||
.select("name_glow")
|
||||
.eq("id", player.id)
|
||||
.single(),
|
||||
).name_glow,
|
||||
"real webhook revoke",
|
||||
240000,
|
||||
);
|
||||
await player.page.reload({ waitUntil: "networkidle" });
|
||||
assert.equal(await player.page.locator("input[type=color]").count(), 0);
|
||||
assert.equal(await player.page.locator(".username-glow").count(), 0);
|
||||
assert.ok((await player.page.locator(".username-bug-finder").count()) > 0);
|
||||
assert.equal(
|
||||
checked(
|
||||
await db
|
||||
.from("profiles")
|
||||
.select("glow_color")
|
||||
.eq("id", player.id)
|
||||
.single(),
|
||||
).glow_color,
|
||||
"#54baff",
|
||||
);
|
||||
await phase("passed");
|
||||
console.log(
|
||||
"Profile appearance production passed: moderator-only prefixes, removal, automatic badges, real webhook grant/revoke, persistent color, authenticated multiplayer/chat, guest spoof prevention and mobile layouts.",
|
||||
);
|
||||
} catch (error) {
|
||||
for (const context of contexts)
|
||||
for (const page of context.pages())
|
||||
if (page.url().includes("/moderation")) {
|
||||
await page
|
||||
.screenshot({
|
||||
path: ".data/support-deploy/appearance-failure.png",
|
||||
fullPage: true,
|
||||
})
|
||||
.catch(() => {});
|
||||
console.error(
|
||||
"Moderator alerts: " +
|
||||
JSON.stringify(
|
||||
await page.locator("[role=alert]").allTextContents(),
|
||||
),
|
||||
);
|
||||
}
|
||||
console.error(error.message);
|
||||
process.exitCode = 1;
|
||||
await phase("failed");
|
||||
} finally {
|
||||
if (roomId)
|
||||
checked(await db.from("multiplayer_rooms").delete().eq("id", roomId));
|
||||
if (createdReport)
|
||||
checked(await db.from("bug_reports").delete().eq("id", reportId));
|
||||
for (const context of contexts)
|
||||
await context.request
|
||||
.post(origin + "/api/v1/auth/logout", { headers: { origin }, data: {} })
|
||||
.catch(() => {});
|
||||
for (const id of accounts) {
|
||||
checked(await db.from("profiles").delete().eq("id", id));
|
||||
checked(await db.auth.admin.deleteUser(id));
|
||||
}
|
||||
await browser.close();
|
||||
}
|
||||
Reference in New Issue
Block a user