287 lines
8.8 KiB
JavaScript
287 lines
8.8 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";
|
|
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();
|
|
}
|