Files
archive-tools/tests/browser/app.spec.ts
T
2026-09-01 02:42:42 +02:00

159 lines
5.4 KiB
TypeScript

import { expect, test, type Page } from "@playwright/test";
import { Buffer } from "node:buffer";
import { strToU8, zipSync } from "fflate";
const ORIGIN = "http://127.0.0.1:4173";
async function localOnly(page: Page) {
const external: string[] = [];
await page.route("**/*", async (route) => {
const url = new URL(route.request().url());
if (url.origin !== ORIGIN) {
external.push(url.href);
await route.abort();
} else await route.continue();
});
return external;
}
test("runs from a nested path without external requests", async ({ page }) => {
const errors: string[] = [];
page.on("pageerror", (error) => errors.push(error.message));
page.on("console", (message) => {
if (message.type() === "error") errors.push(message.text());
});
const external = await localOnly(page);
await page.goto("/deep/nested/archive/");
await expect(
page.getByRole("heading", { name: "Archive Tools" }),
).toBeVisible();
expect(external).toEqual([]);
expect(errors).toEqual([]);
});
test("serves the release identity and hardened headers", async ({
request,
}) => {
const index = await request.get("/deep/nested/archive/");
expect(index.ok()).toBe(true);
expect(index.headers()["content-security-policy"]).toContain(
"default-src 'self'",
);
expect(await index.text()).not.toMatch(/\b(?:src|href)=["']\//u);
const manifest = await request.get("/deep/nested/archive/toolbox-app.json");
await expect(manifest.json()).resolves.toMatchObject({
id: "de.add-ideas.archive-tools",
version: "0.1.0",
entry: "./",
requirements: { workers: false },
});
});
test("inspects, previews and safely repackages a ZIP locally", async ({
page,
}) => {
const external = await localOnly(page);
await page.goto("/deep/nested/archive/");
const archive = zipSync({
"hello.txt": strToU8("Hello from a local ZIP"),
"folder/data.bin": new Uint8Array([0, 1, 2, 3]),
});
await page.getByLabel("Choose archive").setInputFiles({
name: "sample.zip",
mimeType: "application/zip",
buffer: Buffer.from(archive),
});
await expect(page.getByText("Inspected 2 entries locally.")).toBeVisible();
await expect(page.getByRole("button", { name: "hello.txt" })).toBeVisible();
await page.getByRole("button", { name: "hello.txt" }).click();
await expect(page.getByText("Hello from a local ZIP")).toBeVisible();
await page.getByRole("checkbox", { name: "Select hello.txt" }).check();
const downloadPromise = page.waitForEvent("download");
await page
.getByRole("button", { name: "Verify & download safe ZIP" })
.click();
expect((await downloadPromise).suggestedFilename()).toBe(
"sample-safe-selection.zip",
);
expect(external).toEqual([]);
});
test("blocks traversal and case-colliding ZIP paths", async ({ page }) => {
await page.goto("/deep/nested/archive/");
const archive = zipSync({
"../escape.txt": strToU8("bad"),
"Report.txt": strToU8("one"),
"report.TXT": strToU8("two"),
});
await page.getByLabel("Choose archive").setInputFiles({
name: "unsafe.zip",
mimeType: "application/zip",
buffer: Buffer.from(archive),
});
await expect(
page.getByText("Parent-directory path segments are blocked."),
).toBeVisible();
await expect(page.getByText(/Path collides with entry/u)).toHaveCount(2);
await expect(
page.getByRole("checkbox", { name: "Select escape.txt" }),
).toBeDisabled();
await expect(
page.getByRole("checkbox", { name: "Select Report.txt", exact: true }),
).toBeDisabled();
});
test("creates a TAR and compares archive inventories", async ({ page }) => {
await page.goto("/deep/nested/archive/");
await page.getByRole("button", { name: "Create" }).click();
await page.getByLabel("Files").setInputFiles([
{
name: "alpha.txt",
mimeType: "text/plain",
buffer: Buffer.from("alpha"),
},
{
name: "beta.txt",
mimeType: "text/plain",
buffer: Buffer.from("beta"),
},
]);
await page.getByLabel("Output name").fill("browser-created");
await page.getByLabel("Format").selectOption("tar");
const createdPromise = page.waitForEvent("download");
await page.getByRole("button", { name: "Create & download" }).click();
const created = await createdPromise;
expect(created.suggestedFilename()).toBe("browser-created.tar");
await page.getByRole("button", { name: "Inspect & extract" }).click();
await page.getByLabel("Choose archive").setInputFiles(await created.path());
await expect(page.getByText("Inspected 2 entries locally.")).toBeVisible();
await page.getByRole("button", { name: "Compare" }).click();
const left = zipSync({
"same.txt": strToU8("same"),
"changed.txt": strToU8("old"),
});
const right = zipSync({
"same.txt": strToU8("same"),
"changed.txt": strToU8("new"),
});
await page.getByLabel("Left archive").setInputFiles({
name: "left.zip",
mimeType: "application/zip",
buffer: Buffer.from(left),
});
await page.getByLabel("Right archive").setInputFiles({
name: "right.zip",
mimeType: "application/zip",
buffer: Buffer.from(right),
});
await page.getByRole("button", { name: "Compare locally" }).click();
await expect(
page.getByRole("row", { name: /changed\.txt changed/u }),
).toBeVisible();
await expect(page.getByRole("row", { name: /same\.txt/u })).toHaveCount(0);
await page.getByRole("checkbox", { name: "Show changes only" }).uncheck();
await expect(
page.getByRole("row", { name: /same\.txt same/u }),
).toBeVisible();
});