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(); }