All checks were successful
Deploy MineGeo to Dokploy / deploy (push) Successful in 5m57s
382 lines
12 KiB
JavaScript
382 lines
12 KiB
JavaScript
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.site",
|
|
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 === "admin" ? "Prefix admin test" : "Badge player test",
|
|
},
|
|
}),
|
|
);
|
|
const id = result.user.id;
|
|
accounts.push(id);
|
|
checked(
|
|
await db.from("profiles").insert({
|
|
id,
|
|
role,
|
|
display_name:
|
|
role === "admin" ? "Prefix admin 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("admin");
|
|
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-admin 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",
|
|
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: admin-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();
|
|
}
|