152 lines
6.2 KiB
TypeScript
152 lines
6.2 KiB
TypeScript
import { expect, test } from "@playwright/test";
|
||
import "dotenv/config";
|
||
import { createHash } from "node:crypto";
|
||
import { stat } from "node:fs/promises";
|
||
import { prisma } from "@/lib/db";
|
||
import { anhangPfad } from "@/server/services/anhaenge";
|
||
import { SEED_TAG, SEED_VORLAGE, anmelden, kundeWaehlen, variablenFuellen, vorlageWaehlen } from "./helfer";
|
||
|
||
/**
|
||
* Fotos und Dateien an einem Schritt, durch die Oberflaeche (#032).
|
||
*
|
||
* Dieser Ablauf gibt es, weil der Dienst-Test ihn nicht ersetzen konnte. Der
|
||
* Schritt hat **zwei** Felder `datei` in einem Formular — „Foto“ und „Datei“ —,
|
||
* und `FormData.get` lieferte das erste, also das leere: wer „Datei“ waehlte,
|
||
* bekam „Keine Datei gewählt“, waehrend „Foto“ ging. Neunzehn gruene
|
||
* Dienst-Tests sahen davon nichts, weil keiner zwei Felder schickt. Gefunden hat
|
||
* es die Klickprobe am gebauten Stand; festgehalten wird es hier.
|
||
*
|
||
* Geprueft wird deshalb bewusst **ueber den zweiten Knopf** und danach, ob es
|
||
* wirklich steht: Neuladen, Zeile in der Datenbank, Datei auf der Platte, und
|
||
* die Pruefsumme der ausgelieferten Bytes gegen die gespeicherte.
|
||
*/
|
||
|
||
const STEMPEL = Date.now();
|
||
|
||
/* Ein echtes 2x2-PNG. Eine leere Datei wuerde der Dienst zu Recht abweisen. */
|
||
const PNG = Buffer.from(
|
||
"iVBORw0KGgoAAAANSUhEUgAAAAIAAAACCAYAAACZgbYnAAAAEUlEQVR4nGP8z8DAwMDEwAAAEAAHAAo7/UkAAAAASUVORK5CYII=",
|
||
"base64",
|
||
);
|
||
|
||
/* Der zweite Ablauf arbeitet am Lauf des ersten weiter – also der Reihe nach. */
|
||
test.describe.serial("Anhänge an einem Schritt", () => {
|
||
let laufId = "";
|
||
|
||
test("über „Datei“ hochladen, neu laden, entfernen", async ({ page }) => {
|
||
test.slow();
|
||
await anmelden(page);
|
||
|
||
await page.goto("/laeufe/starten");
|
||
await vorlageWaehlen(page, SEED_VORLAGE);
|
||
const geraet = await prisma.geraet.findUniqueOrThrow({
|
||
where: { tagCode: SEED_TAG },
|
||
select: { id: true, kundeId: true },
|
||
});
|
||
await kundeWaehlen(page, geraet.kundeId);
|
||
await page.selectOption('select[name="geraetId"]', geraet.id);
|
||
await page.locator("details summary", { hasText: "Titel, Ticket" }).click();
|
||
await page.getByLabel("Titel", { exact: true }).fill(`ZZ E2E Anhang ${STEMPEL}`);
|
||
await variablenFuellen(page);
|
||
await page.getByRole("button", { name: "Lauf anlegen" }).click();
|
||
await page.waitForURL(/\/laeufe\/(?!starten)[^/]+$/, { timeout: 30_000 });
|
||
laufId = new URL(page.url()).pathname.split("/").pop() ?? "";
|
||
|
||
/*
|
||
* Beginnen, wie der Techniker es taete. Anhaengen ginge auch am offenen
|
||
* Lauf — `lesend` haengt am Archiv und am Recht, nicht am Status —, aber
|
||
* genau so wird die Oberflaeche benutzt.
|
||
*/
|
||
await page.getByRole("button", { name: "Lauf beginnen" }).click();
|
||
await expect(page.getByRole("button", { name: "Lauf abschließen" })).toBeVisible();
|
||
|
||
for (const kopf of await page.locator("button[aria-expanded]").all()) {
|
||
if ((await kopf.getAttribute("aria-expanded")) === "false") await kopf.click();
|
||
}
|
||
|
||
/*
|
||
* Feld 0 ist „Foto“, Feld 1 ist „Datei“ – beide heissen `datei` und stehen
|
||
* im selben Formular. Genau der zweite Weg war kaputt, also geht der Ablauf
|
||
* ueber ihn.
|
||
*/
|
||
const felder = page.locator('input[name="datei"]');
|
||
expect(await felder.count()).toBeGreaterThan(1);
|
||
await felder.nth(1).setInputFiles({ name: "serverraum.png", mimeType: "image/png", buffer: PNG });
|
||
|
||
const miniatur = page.locator('img[src^="/api/anhaenge/"]').first();
|
||
await expect(miniatur).toBeVisible({ timeout: 30_000 });
|
||
await expect(page.getByText("serverraum.png").first()).toBeVisible();
|
||
|
||
// Neu laden: steht es wirklich, oder nur im Zustand der Seite?
|
||
await page.reload();
|
||
await expect(page.locator('img[src^="/api/anhaenge/"]').first()).toBeVisible({ timeout: 30_000 });
|
||
|
||
const zeile = await prisma.anhang.findFirstOrThrow({
|
||
where: { laufId },
|
||
select: {
|
||
id: true,
|
||
kundeId: true,
|
||
laufId: true,
|
||
dateiname: true,
|
||
medientyp: true,
|
||
groesse: true,
|
||
pruefsumme: true,
|
||
},
|
||
});
|
||
expect(zeile).toMatchObject({
|
||
dateiname: "serverraum.png",
|
||
medientyp: "image/png",
|
||
groesse: PNG.byteLength,
|
||
pruefsumme: createHash("sha256").update(PNG).digest("hex"),
|
||
});
|
||
|
||
// Und auf der Platte, unter dem Pfad aus der Kennung.
|
||
const pfad = anhangPfad(zeile);
|
||
expect((await stat(pfad)).size).toBe(PNG.byteLength);
|
||
|
||
// Die Auslieferung prueft Recht und Mandant selbst – und liefert die Bytes,
|
||
// die hereingingen.
|
||
const antwort = await page.request.get(`/api/anhaenge/${zeile.id}`);
|
||
expect(antwort.status()).toBe(200);
|
||
expect(antwort.headers()["content-type"]).toBe("image/png");
|
||
expect(antwort.headers()["x-content-type-options"]).toBe("nosniff");
|
||
expect(
|
||
createHash("sha256")
|
||
.update(await antwort.body())
|
||
.digest("hex"),
|
||
).toBe(zeile.pruefsumme);
|
||
|
||
// Entfernen nimmt Zeile **und** Datei.
|
||
await page.getByRole("button", { name: "serverraum.png entfernen" }).click();
|
||
await expect(page.locator('img[src^="/api/anhaenge/"]')).toHaveCount(0, { timeout: 30_000 });
|
||
expect(await prisma.anhang.count({ where: { laufId } })).toBe(0);
|
||
await expect(stat(pfad)).rejects.toThrow();
|
||
});
|
||
|
||
test("weist ein SVG ab, auch wenn es am Dateiwähler vorbei kommt", async ({ page }) => {
|
||
test.slow();
|
||
await anmelden(page);
|
||
expect(laufId).not.toBe("");
|
||
await page.goto(`/laeufe/${laufId}`);
|
||
for (const kopf of await page.locator("button[aria-expanded]").all()) {
|
||
if ((await kopf.getAttribute("aria-expanded")) === "false") await kopf.click();
|
||
}
|
||
await page
|
||
.locator('input[name="datei"]')
|
||
.nth(1)
|
||
.setInputFiles({
|
||
name: "boeses.svg",
|
||
mimeType: "image/svg+xml",
|
||
buffer: Buffer.from('<svg xmlns="http://www.w3.org/2000/svg"><script>alert(1)</script></svg>'),
|
||
});
|
||
|
||
/*
|
||
* `accept` haelt im Dateiwaehler nur die Finger ab. Dass ein SVG nicht
|
||
* hereinkommt, muss der Server sagen – ein SVG ist ein Dokument mit
|
||
* Skripten, das ein Betrachter ausfuehrt.
|
||
*/
|
||
await expect(page.getByText(/lassen sich nicht anhängen/)).toBeVisible({ timeout: 30_000 });
|
||
expect(await prisma.anhang.count({ where: { laufId } })).toBe(0);
|
||
});
|
||
});
|