All checks were successful
Deploy MineGeo to Dokploy / deploy (push) Successful in 5m57s
260 lines
8.4 KiB
TypeScript
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.site").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.");
|
|
}
|