Files
MineGeo/scripts/test-map-sharing-browser.ts

260 lines
8.4 KiB
TypeScript

import assert from "node:assert/strict";
import { randomUUID } from "node:crypto";
import fs from "node:fs";
import { chromium } from "@playwright/test";
import { createClient } from "@supabase/supabase-js";
import sharp from "sharp";
import { datasetSchema, faces } from "../shared/protocol";
import { readMapArchive } from "../shared/map-archive";
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 output = ".data/sharing-browser";
fs.mkdirSync(output, { recursive: true });
const records: {
users: string[];
datasets: string[];
server?: string;
assets: string[];
} = { users: [], datasets: [], assets: [] };
const save = () =>
fs.writeFileSync(`${output}/fixtures.json`, JSON.stringify(records));
const checked = <T>(result: { data: T; error: { message: string } | null }) => {
if (result.error) throw new Error(result.error.message);
return result.data!;
};
const run = randomUUID();
let browser: Awaited<ReturnType<typeof chromium.launch>> | undefined;
try {
const accounts = [];
for (const role of ["Owner", "Friend"]) {
const email = `zip-${role.toLowerCase()}-${run}@example.com`,
password = `Zip-test-${randomUUID()}!`;
const created = await db.auth.admin.createUser({
email,
password,
email_confirm: true,
user_metadata: { display_name: `ZIP verification ${role}` },
});
if (created.error || !created.data.user)
throw new Error("Could not create a temporary fixture account");
const account = created.data.user;
accounts.push({ email, password, id: account.id });
records.users.push(account.id);
save();
checked(
await db
.from("profiles")
.upsert({ id: account.id, display_name: `ZIP verification ${role}` }),
);
}
const viewpoints = Array.from({ length: 5 }, (_, i) => ({
id: `p${i}`,
x: i,
y: 70,
z: i,
}));
const manifest = datasetSchema.parse({
format: "minegeo-dataset/v1",
minecraftVersion: "26.2",
dimension: "minecraft:overworld",
source: "plugin",
name: "ZIP verification world",
description: "Temporary browser fixture",
serverName: `ZIP verification ${run}`,
bounds: { minX: 0, minZ: 0, maxX: 32, maxZ: 32 },
north: "-z",
viewpoints,
assets: [{ path: "terrain.json.gz", bytes: 1, sha256: "0".repeat(64) }],
});
const original = checked(
await db.rpc("create_upload", {
p_owner: accounts[0].id,
p_manifest: manifest,
}),
);
records.datasets.push(original);
save();
records.server = checked(
await db.from("datasets").select("server_id").eq("id", original).single(),
).server_id;
save();
const image = await sharp({
create: { width: 256, height: 256, channels: 3, background: "#729343" },
})
.webp()
.toBuffer();
records.assets = [
"map.webp",
...viewpoints.flatMap((p) => [
...faces.map((f) => `${p.id}/${f}.webp`),
`${p.id}/preview.webp`,
]),
];
save();
for (const path of records.assets)
checked(
await db.storage
.from("minegeo-media")
.upload(`${original}/${path}`, image, { contentType: "image/webp" }),
);
checked(
await db
.from("dataset_inputs")
.update({ validated: true })
.eq("dataset_id", original),
);
checked(
await db
.from("datasets")
.update({
state: "draft",
panorama_count: 5,
map_path: `${original}/map.webp`,
cover_path: `${original}/p0/preview.webp`,
})
.eq("id", original),
);
browser = await chromium.launch({ headless: true });
const owner = await browser.newContext({ locale: "en-US" });
const friend = await browser.newContext({ locale: "en-US" });
for (const [index, context] of [owner, friend].entries()) {
const response = await context.request.post(`${origin}/api/v1/auth`, {
headers: { origin },
data: {
action: "login",
email: accounts[index].email,
password: accounts[index].password,
},
});
assert.equal(response.status(), 200, "Fixture login failed");
}
const ownerPage = await owner.newPage();
await ownerPage.goto(`${origin}/dashboard/${original}`);
const downloadEvent = ownerPage.waitForEvent("download");
await ownerPage
.getByRole("button", { name: "Export ZIP", exact: true })
.click();
const download = await downloadEvent;
const zipPath = `${output}/map.zip`;
await download.saveAs(zipPath);
const restored = await readMapArchive(
new Uint8Array(fs.readFileSync(zipPath)),
);
assert.equal(restored.archive.originalOwner, "ZIP verification Owner");
assert.equal(restored.archive.source, "plugin");
console.log("Owner exported a portable ZIP in the browser.");
const page = await friend.newPage();
await page.goto(`${origin}/servers`);
await page.getByRole("button", { name: "Import", exact: true }).click();
await page.getByLabel("Map ZIP file", { exact: true }).setInputFiles(zipPath);
await page.waitForURL(/\/dashboard\/[0-9a-f-]{36}$/, { timeout: 120000 });
const copy = page.url().split("/").pop()!;
records.datasets.push(copy);
save();
await page
.getByRole("heading", { name: "Owned by ZIP verification Owner" })
.waitFor();
await page
.getByRole("button", { name: "Start exploring", exact: true })
.waitFor({ timeout: 120000 });
await page.screenshot({
path: `${output}/imported-desktop.png`,
fullPage: true,
});
assert.equal(
await page.getByRole("button", { name: "Export ZIP", exact: true }).count(),
0,
);
assert.equal(
await page
.getByRole("button", { name: "Submit for review →", exact: true })
.isDisabled(),
true,
);
const blocked = await friend.request.post(
`${origin}/api/v1/datasets/${copy}/submit`,
{ headers: { origin }, data: {} },
);
assert.equal(blocked.status(), 403);
assert.match(
(await blocked.json()).error,
/Owned by "ZIP verification Owner"/,
);
const foreignExport = await friend.request.get(
`${origin}/api/v1/datasets/${original}/export`,
);
assert.equal(foreignExport.status(), 404);
const foreignCopy = await owner.request.post(`${origin}/api/v1/sessions`, {
headers: { origin },
data: { datasetId: copy, difficulty: "normal" },
});
assert.notEqual(foreignCopy.status(), 200);
console.log(
"Friend imported and processed the ZIP; original-owner attribution, export restriction, and submission block passed.",
);
await page
.getByRole("button", { name: "Start exploring", exact: true })
.click();
await page.waitForURL(/\/play\//);
const sessionId = page.url().split("/").pop()!;
const sessionResponse = await friend.request.get(
`${origin}/api/v1/sessions/${sessionId}`,
);
assert.equal(sessionResponse.status(), 200);
const session = await sessionResponse.json();
assert.equal(session.rounds.length, 5);
assert.ok(
session.rounds.every(
(r: { faces: string[] }) =>
r.faces.length === 6 && r.faces.every(Boolean),
),
);
const roomResponse = await friend.request.post(`${origin}/api/v1/rooms`, {
headers: { origin },
data: {
datasetId: copy,
difficulty: "normal",
displayName: "ZIP Friend",
playerLimit: 4,
},
});
assert.equal(roomResponse.status(), 200, "Private room failed");
const roomId = (await roomResponse.json()).id;
const invited = await browser.newContext();
const join = await invited.request.post(
`${origin}/api/v1/rooms/${roomId}/join`,
{ headers: { origin }, data: { displayName: "ZIP Guest" } },
);
assert.equal(join.status(), 200);
console.log(
"Private solo game and invited multiplayer guest passed without moderation.",
);
await page.setViewportSize({ width: 390, height: 844 });
await page.goto(`${origin}/servers`);
await page.getByRole("button", { name: "Import", exact: true }).click();
await page.getByRole("heading", { name: "Import a shared map" }).waitFor();
await page.screenshot({
path: `${output}/import-mobile.png`,
fullPage: true,
});
assert.equal(
await page.evaluate(
() => document.documentElement.scrollWidth > window.innerWidth,
),
false,
);
console.log("Mobile import dialog passed at 390px.");
} finally {
await browser?.close();
save();
console.log("Temporary fixture IDs saved for cleanup.");
}