#038: Die E2E-Ablaeufe laufen ab jetzt vor jedem Merge nach main, nicht vor jedem Commit. Gemessen: Bau 14 s, Start 7 s, Ablaeufe 256 s — zu den vier Minuten des bestehenden Tores. Neun Minuten vor jedem Commit ist ein Tor, das umgangen wird. Vor jedem Merge waeren es in Phase 2 acht Laeufe gewesen, und jeder der vier Befunde aus #027 waere am Tag seiner Entstehung aufgefallen. Geprueft wurde jeder der 16 Ablaeufe gegen die Frage, ob es ein Vitest-Gegenstueck gibt. Neun Zusagen haengen ausschliesslich dort, und keine laesst sich holen: alle brauchen einen echten Browser. Die Liste steht in CLAUDE.md. Der eigentliche Fund dabei war keine Testluecke, sondern eine Konvention ohne Pruefer: "jede Seite prueft selbst" steht seit #007 in CLAUDE.md, und geprueft hat es bis hierher nur ein Mensch, der nachsieht. Der Review von Phase 2 hat alle 27 Seiten durchgesehen — ein Ergebnis, das fuer genau einen Tag gilt. tests/zugang.test.ts liest sie jetzt, samt jedem Route Handler; die Ausnahmen (Auth, Healthcheck) stehen namentlich mit Begruendung, und ein Test prueft auch die Gegenrichtung. #037: Das Lesemodell der Anhaenge liegt in queries/ und engt selbst ein — vorher hing das daran, dass beide Aufrufer den Lauf vorher pruefen. /meine-geraete wird bei 390 px gemessen, mit einem echten MITARBEITER-Konto aus dem E2E-Vorlauf, dazu ein Test, dass dort kein Link in die Verwaltung angeboten wird. Dabei selbst einen Fehler gemacht, der die neue Regel begruendet: fuer das zweite Konto brauchte `anmelden` eine Variante, die nicht auf / wartet, und ich habe das Warten fuer alle gelockert. Das machte zwei unverwandte Ablaeufe unzuverlaessig — sichtbar nur im vollen Durchgang, einzeln waren beide gruen. Ein gemeinsamer Helfer in einer Testsuite ist geteilter Zustand. Ohne vollen Durchgang haette ich zwei rote Ablaeufe gemergt. 1457 Vitest-Tests (vorher 1454), 115 E2E-Ablaeufe (vorher 113). Offen bleibt nur noch #024, und das verlangt einen echten Proxmox-Host. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
535 lines
18 KiB
TypeScript
535 lines
18 KiB
TypeScript
import { mkdtemp, readFile, rm, stat } from "node:fs/promises";
|
||
import { tmpdir } from "node:os";
|
||
import { join } from "node:path";
|
||
import { afterAll, beforeAll, beforeEach, describe, expect, it } from "vitest";
|
||
import { Betriebsmodus, LaufStatus, VersionStatus } from "@/generated/prisma/enums";
|
||
import {
|
||
dateinameSaeubern,
|
||
gewaehlteDatei,
|
||
groesseText,
|
||
istBild,
|
||
istErlaubterMedientyp,
|
||
} from "@/lib/anhaenge";
|
||
import {
|
||
AnhangFehler,
|
||
anhangEntfernen,
|
||
anhangLesen,
|
||
anhangPfad,
|
||
anhangSpeichern,
|
||
} from "@/server/services/anhaenge";
|
||
import { anhaengeDesLaufs } from "@/server/queries/anhaenge";
|
||
import { MandantFehler } from "@/server/mandant";
|
||
import { LaufFehler } from "@/server/services/laeufe";
|
||
import { datenbankLeeren, datenbankVerfuegbar, testPrisma } from "./helpers/db";
|
||
|
||
/**
|
||
* Anhaenge und Fotos an Schritten (#032).
|
||
*
|
||
* Vier Zusagen tragen das Ganze: der Archiv-Schreibschutz gilt auch hier, ein
|
||
* fremder Kunde kommt nicht heran, die Grenzen greifen mit einer Begruendung,
|
||
* und eine Datei ohne Zeile bleibt nicht liegen.
|
||
*
|
||
* Die Dateien landen in einem Wegwerf-Verzeichnis — `ANHAENGE_PFAD` wird fuer
|
||
* diese Datei umgebogen und danach wieder weggeraeumt.
|
||
*/
|
||
|
||
const VORHER = process.env["ANHAENGE_PFAD"];
|
||
let verzeichnis = "";
|
||
|
||
beforeAll(async () => {
|
||
verzeichnis = await mkdtemp(join(tmpdir(), "laufzettel-anhaenge-"));
|
||
process.env["ANHAENGE_PFAD"] = verzeichnis;
|
||
});
|
||
|
||
afterAll(async () => {
|
||
if (VORHER === undefined) delete process.env["ANHAENGE_PFAD"];
|
||
else process.env["ANHAENGE_PFAD"] = VORHER;
|
||
if (verzeichnis) await rm(verzeichnis, { recursive: true, force: true });
|
||
});
|
||
|
||
describe("Formen und Grenzen", () => {
|
||
it("erlaubt Bilder und PDF, aber kein SVG", () => {
|
||
// SVG ist ein Dokument mit Skripten, das ein Betrachter ausführt. Ein
|
||
// Anhang, den jemand öffnet, darf kein Angriffsweg sein.
|
||
expect(istErlaubterMedientyp("image/jpeg")).toBe(true);
|
||
expect(istErlaubterMedientyp("application/pdf")).toBe(true);
|
||
expect(istErlaubterMedientyp("image/svg+xml")).toBe(false);
|
||
expect(istErlaubterMedientyp("text/html")).toBe(false);
|
||
});
|
||
|
||
it("zeigt nur Bilder als Miniatur", () => {
|
||
expect(istBild("image/png")).toBe(true);
|
||
expect(istBild("application/pdf")).toBe(false);
|
||
});
|
||
|
||
it("macht aus einem Pfad einen Dateinamen und wirft Steuerzeichen weg", () => {
|
||
// Der Name wird nie zum Pfad – trotzdem landet er in einer Kopfzeile.
|
||
expect(dateinameSaeubern("../../etc/passwd")).toBe("passwd");
|
||
expect(dateinameSaeubern("C:\\Users\\x\\foto.jpg")).toBe("foto.jpg");
|
||
expect(dateinameSaeubern('fo"to\u0000.jpg')).toBe("foto.jpg");
|
||
expect(dateinameSaeubern(" ")).toBe("anhang");
|
||
expect(dateinameSaeubern("x".repeat(300))).toHaveLength(200);
|
||
});
|
||
|
||
it("schreibt Größen deutsch", () => {
|
||
expect(groesseText(512)).toBe("512 B");
|
||
expect(groesseText(2048)).toBe("2 KB");
|
||
expect(groesseText(3 * 1024 * 1024 + 100_000)).toBe("3,1 MB");
|
||
});
|
||
|
||
it("nimmt aus zwei Feldern „datei“ das nicht leere", () => {
|
||
/*
|
||
* Der Schritt hat zwei Felder „datei“ – „Foto“ und „Datei“ – in einem
|
||
* Formular, also kommt immer auch das leere mit. `FormData.get` lieferte
|
||
* das erste, und wer „Datei“ wählte, bekam „Keine Datei gewählt“. Gefunden
|
||
* in der Klickprobe am gebauten Stand; kein Dienst-Test schickt zwei
|
||
* Felder.
|
||
*/
|
||
const leer = new File([], "");
|
||
const echt = new File([new Uint8Array([1, 2, 3])], "foto.png", { type: "image/png" });
|
||
|
||
expect(gewaehlteDatei([leer, echt])?.name).toBe("foto.png");
|
||
expect(gewaehlteDatei([echt, leer])?.name).toBe("foto.png");
|
||
expect(gewaehlteDatei([leer, leer])).toBeUndefined();
|
||
expect(gewaehlteDatei(["nicht einmal eine Datei", null])).toBeUndefined();
|
||
});
|
||
});
|
||
|
||
async function aufbau() {
|
||
const kunde = await testPrisma.kunde.create({
|
||
data: { name: "Eigene Organisation", kuerzel: "EIGEN", istEigeneOrganisation: true },
|
||
});
|
||
await testPrisma.einstellung.create({ data: { id: "global", modus: Betriebsmodus.INTERN } });
|
||
const admin = await testPrisma.benutzer.create({
|
||
data: { email: "admin@firma.example", name: "Admin", rolle: "ADMIN" },
|
||
});
|
||
|
||
const vorlage = await testPrisma.vorlage.create({ data: { name: "PC Neuinstallation" } });
|
||
const version = await testPrisma.vorlagenVersion.create({
|
||
data: { vorlageId: vorlage.id, version: 1, status: VersionStatus.ENTWURF },
|
||
});
|
||
const abschnitt = await testPrisma.vorlagenAbschnitt.create({
|
||
data: { versionId: version.id, titel: "Vorbereitung", position: 0 },
|
||
});
|
||
const schritt = await testPrisma.vorlagenSchritt.create({
|
||
data: { abschnittId: abschnitt.id, titel: "Typenschild fotografieren", position: 0 },
|
||
});
|
||
await testPrisma.vorlagenVersion.update({
|
||
where: { id: version.id },
|
||
data: { status: VersionStatus.VEROEFFENTLICHT, veroeffentlichtAm: new Date() },
|
||
});
|
||
|
||
const lauf = await testPrisma.lauf.create({
|
||
data: { vorlagenVersionId: version.id, titel: "Ein Lauf", kundeId: kunde.id },
|
||
});
|
||
const ergebnis = await testPrisma.schrittergebnis.create({
|
||
data: { laufId: lauf.id, vorlagenSchrittId: schritt.id },
|
||
});
|
||
|
||
return {
|
||
kunde,
|
||
admin: { id: admin.id, rolle: admin.rolle },
|
||
laufId: lauf.id,
|
||
schrittId: schritt.id,
|
||
ergebnisId: ergebnis.id,
|
||
};
|
||
}
|
||
|
||
/** Ein winziges, gültiges PNG. */
|
||
const PNG = Buffer.from(
|
||
"89504e470d0a1a0a0000000d49484452000000010000000108060000001f15c489" +
|
||
"0000000a49444154789c6300010000050001" +
|
||
"0d0a2db40000000049454e44ae426082",
|
||
"hex",
|
||
);
|
||
|
||
describe.runIf(datenbankVerfuegbar)("Anhänge", () => {
|
||
beforeEach(async () => {
|
||
await datenbankLeeren();
|
||
});
|
||
|
||
it("legt eine Datei ab und schreibt die Metadaten", async () => {
|
||
const { admin, laufId, schrittId, ergebnisId, kunde } = await aufbau();
|
||
const { id } = await anhangSpeichern(
|
||
{
|
||
laufId,
|
||
vorlagenSchrittId: schrittId,
|
||
dateiname: "typenschild.png",
|
||
medientyp: "image/png",
|
||
inhalt: new Uint8Array(PNG),
|
||
},
|
||
admin,
|
||
);
|
||
|
||
const zeile = await testPrisma.anhang.findUniqueOrThrow({ where: { id } });
|
||
expect(zeile).toMatchObject({
|
||
schrittergebnisId: ergebnisId,
|
||
laufId,
|
||
kundeId: kunde.id,
|
||
dateiname: "typenschild.png",
|
||
medientyp: "image/png",
|
||
groesse: PNG.byteLength,
|
||
erstelltVonId: admin.id,
|
||
});
|
||
// SHA-256 als Hex – eine CHECK-Bedingung hält die Form fest.
|
||
expect(zeile.pruefsumme).toMatch(/^[0-9a-f]{64}$/);
|
||
|
||
// Die Datei liegt wirklich da, und zwar unter einem Pfad aus der Kennung.
|
||
const pfad = anhangPfad(zeile);
|
||
expect(pfad).toContain(kunde.id);
|
||
expect(pfad).toContain(laufId);
|
||
expect(pfad).not.toContain("typenschild");
|
||
expect((await stat(pfad)).size).toBe(PNG.byteLength);
|
||
expect(await readFile(pfad)).toEqual(PNG);
|
||
});
|
||
|
||
it("liest den Inhalt wieder zurück", async () => {
|
||
const { admin, laufId, schrittId } = await aufbau();
|
||
const { id } = await anhangSpeichern(
|
||
{
|
||
laufId,
|
||
vorlagenSchrittId: schrittId,
|
||
dateiname: "a.png",
|
||
medientyp: "image/png",
|
||
inhalt: new Uint8Array(PNG),
|
||
},
|
||
admin,
|
||
);
|
||
|
||
const datei = await anhangLesen(id, admin);
|
||
expect(datei.inhalt).toEqual(PNG);
|
||
expect(datei.medientyp).toBe("image/png");
|
||
expect(datei.dateiname).toBe("a.png");
|
||
});
|
||
|
||
it("weist einen fremden Medientyp mit Begründung ab", async () => {
|
||
const { admin, laufId, schrittId } = await aufbau();
|
||
await expect(
|
||
anhangSpeichern(
|
||
{
|
||
laufId,
|
||
vorlagenSchrittId: schrittId,
|
||
dateiname: "boese.svg",
|
||
medientyp: "image/svg+xml",
|
||
inhalt: new Uint8Array(PNG),
|
||
},
|
||
admin,
|
||
),
|
||
).rejects.toThrow(/lassen sich nicht anhängen/);
|
||
});
|
||
|
||
it("weist eine zu große Datei mit Größen in der Meldung ab", async () => {
|
||
const vorher = process.env["ANHANG_MAX_MB"];
|
||
process.env["ANHANG_MAX_MB"] = "1";
|
||
try {
|
||
const { admin, laufId, schrittId } = await aufbau();
|
||
await expect(
|
||
anhangSpeichern(
|
||
{
|
||
laufId,
|
||
vorlagenSchrittId: schrittId,
|
||
dateiname: "gross.png",
|
||
medientyp: "image/png",
|
||
inhalt: new Uint8Array(2 * 1024 * 1024),
|
||
},
|
||
admin,
|
||
),
|
||
).rejects.toThrow(/2,0 MB groß.*höchstens 1,0 MB/s);
|
||
} finally {
|
||
if (vorher === undefined) delete process.env["ANHANG_MAX_MB"];
|
||
else process.env["ANHANG_MAX_MB"] = vorher;
|
||
}
|
||
});
|
||
|
||
it("weist eine leere Datei ab", async () => {
|
||
const { admin, laufId, schrittId } = await aufbau();
|
||
await expect(
|
||
anhangSpeichern(
|
||
{
|
||
laufId,
|
||
vorlagenSchrittId: schrittId,
|
||
dateiname: "leer.png",
|
||
medientyp: "image/png",
|
||
inhalt: new Uint8Array(0),
|
||
},
|
||
admin,
|
||
),
|
||
).rejects.toThrow(/leer/);
|
||
});
|
||
|
||
it("begrenzt die Anzahl je Lauf", async () => {
|
||
const vorher = process.env["ANHAENGE_JE_LAUF"];
|
||
process.env["ANHAENGE_JE_LAUF"] = "2";
|
||
try {
|
||
const { admin, laufId, schrittId } = await aufbau();
|
||
const eingabe = {
|
||
laufId,
|
||
vorlagenSchrittId: schrittId,
|
||
dateiname: "a.png",
|
||
medientyp: "image/png",
|
||
inhalt: new Uint8Array(PNG),
|
||
};
|
||
await anhangSpeichern(eingabe, admin);
|
||
await anhangSpeichern(eingabe, admin);
|
||
await expect(anhangSpeichern(eingabe, admin)).rejects.toThrow(/Mehr als 2 je Lauf/);
|
||
} finally {
|
||
if (vorher === undefined) delete process.env["ANHAENGE_JE_LAUF"];
|
||
else process.env["ANHAENGE_JE_LAUF"] = vorher;
|
||
}
|
||
});
|
||
|
||
it("weist an einem archivierten Lauf ab – Hochladen und Entfernen", async () => {
|
||
/*
|
||
* Der Archiv-Schreibschutz sitzt an genau einer Stelle
|
||
* (`laufZumSchreibenHolen` → `laufMussVeraenderbarSein`). Dass er auch hier
|
||
* greift, ist das Kriterium aus dem Issue.
|
||
*/
|
||
const { admin, laufId, schrittId } = await aufbau();
|
||
const { id } = await anhangSpeichern(
|
||
{
|
||
laufId,
|
||
vorlagenSchrittId: schrittId,
|
||
dateiname: "a.png",
|
||
medientyp: "image/png",
|
||
inhalt: new Uint8Array(PNG),
|
||
},
|
||
admin,
|
||
);
|
||
// Eine CHECK-Bedingung verlangt zu einem archivierten Lauf auch einen
|
||
// Abschlusszeitpunkt: archiviert wird nur, was fertig ist.
|
||
await testPrisma.lauf.update({
|
||
where: { id: laufId },
|
||
data: {
|
||
status: LaufStatus.ARCHIVIERT,
|
||
gestartetAm: new Date(),
|
||
abgeschlossenAm: new Date(),
|
||
archiviertAm: new Date(),
|
||
},
|
||
});
|
||
|
||
await expect(
|
||
anhangSpeichern(
|
||
{
|
||
laufId,
|
||
vorlagenSchrittId: schrittId,
|
||
dateiname: "b.png",
|
||
medientyp: "image/png",
|
||
inhalt: new Uint8Array(PNG),
|
||
},
|
||
admin,
|
||
),
|
||
).rejects.toThrow(LaufFehler);
|
||
await expect(anhangEntfernen(id, admin)).rejects.toThrow(/schreibgeschützt/);
|
||
|
||
// Lesen bleibt erlaubt: ein Foto vom Typenschild ist Teil des Belegs.
|
||
expect((await anhangLesen(id, admin)).inhalt).toEqual(PNG);
|
||
});
|
||
|
||
it("weist einen Schritt ab, der nicht zu diesem Lauf gehört", async () => {
|
||
const { admin, laufId } = await aufbau();
|
||
await expect(
|
||
anhangSpeichern(
|
||
{
|
||
laufId,
|
||
vorlagenSchrittId: "gibt-es-nicht",
|
||
dateiname: "a.png",
|
||
medientyp: "image/png",
|
||
inhalt: new Uint8Array(PNG),
|
||
},
|
||
admin,
|
||
),
|
||
).rejects.toThrow(/Schritt gibt es in diesem Lauf nicht/);
|
||
});
|
||
|
||
it("entfernt Zeile und Datei zusammen", async () => {
|
||
const { admin, laufId, schrittId } = await aufbau();
|
||
const { id } = await anhangSpeichern(
|
||
{
|
||
laufId,
|
||
vorlagenSchrittId: schrittId,
|
||
dateiname: "a.png",
|
||
medientyp: "image/png",
|
||
inhalt: new Uint8Array(PNG),
|
||
},
|
||
admin,
|
||
);
|
||
const zeile = await testPrisma.anhang.findUniqueOrThrow({ where: { id } });
|
||
const pfad = anhangPfad(zeile);
|
||
|
||
await anhangEntfernen(id, admin);
|
||
expect(await testPrisma.anhang.findUnique({ where: { id } })).toBeNull();
|
||
await expect(stat(pfad)).rejects.toThrow();
|
||
});
|
||
|
||
it("sagt, wenn die Datei fehlt – und nennt die Sicherung als Grund", async () => {
|
||
// Der Fall nach einer Sicherung, die nur die Datenbank mitgenommen hat.
|
||
const { admin, laufId, schrittId } = await aufbau();
|
||
const { id } = await anhangSpeichern(
|
||
{
|
||
laufId,
|
||
vorlagenSchrittId: schrittId,
|
||
dateiname: "a.png",
|
||
medientyp: "image/png",
|
||
inhalt: new Uint8Array(PNG),
|
||
},
|
||
admin,
|
||
);
|
||
const zeile = await testPrisma.anhang.findUniqueOrThrow({ where: { id } });
|
||
await rm(anhangPfad(zeile));
|
||
|
||
await expect(anhangLesen(id, admin)).rejects.toThrow(/sicherung\.md/);
|
||
});
|
||
|
||
it("zählt die Anhänge eines Laufs mit Urheber", async () => {
|
||
const { admin, laufId, schrittId, ergebnisId } = await aufbau();
|
||
await anhangSpeichern(
|
||
{
|
||
laufId,
|
||
vorlagenSchrittId: schrittId,
|
||
dateiname: "eins.png",
|
||
medientyp: "image/png",
|
||
inhalt: new Uint8Array(PNG),
|
||
},
|
||
admin,
|
||
);
|
||
|
||
const liste = await anhaengeDesLaufs(laufId, admin);
|
||
expect(liste).toHaveLength(1);
|
||
expect(liste[0]).toMatchObject({
|
||
schrittergebnisId: ergebnisId,
|
||
dateiname: "eins.png",
|
||
erstelltVon: "Admin",
|
||
});
|
||
});
|
||
|
||
it("engt die Liste selbst auf die sichtbaren Kunden ein – ohne Vorprüfung des Aufrufers", async () => {
|
||
/*
|
||
* **Die Zusage von #037.** Vorher nahm `anhaengeDesLaufs` nur eine
|
||
* Lauf-Kennung und wendete keine Sicht an; dass niemand fremde Anhänge sah,
|
||
* hing daran, dass beide Aufrufer den Lauf vorher prüften. Das war kein
|
||
* Schaden und trotzdem die falsche Form: ein Lesemodell wendet seine Sicht
|
||
* selbst an, **damit kein Aufrufer sie vergessen kann** (CLAUDE.md).
|
||
*
|
||
* Dieser Test ruft die Funktion deshalb **direkt** und ohne jede
|
||
* Vorprüfung — so wie es der dritte Aufrufer täte, der die Prüfung vergisst.
|
||
*/
|
||
const vorher = process.env["OIDC_CUSTOMER_GROUP_PREFIX"];
|
||
process.env["OIDC_CUSTOMER_GROUP_PREFIX"] = "kunde-";
|
||
try {
|
||
const { admin, laufId, schrittId } = await aufbau();
|
||
await testPrisma.einstellung.update({
|
||
where: { id: "global" },
|
||
data: { modus: Betriebsmodus.DIENSTLEISTUNG },
|
||
});
|
||
await anhangSpeichern(
|
||
{
|
||
laufId,
|
||
vorlagenSchrittId: schrittId,
|
||
dateiname: "fremd.png",
|
||
medientyp: "image/png",
|
||
inhalt: new Uint8Array(PNG),
|
||
},
|
||
admin,
|
||
);
|
||
|
||
const fremd = await testPrisma.kunde.create({ data: { name: "Fremd GmbH", kuerzel: "FRD" } });
|
||
const techniker = await testPrisma.benutzer.create({
|
||
data: { email: "tina@firma.example", name: "Tina", rolle: "TECHNIKER" },
|
||
});
|
||
await testPrisma.benutzerKunde.create({
|
||
data: { benutzerId: techniker.id, kundeId: fremd.id },
|
||
});
|
||
|
||
// Der Administrator sieht den Anhang.
|
||
expect(await anhaengeDesLaufs(laufId, admin)).toHaveLength(1);
|
||
// Der auf einen anderen Kunden beschränkte Techniker nicht.
|
||
expect(await anhaengeDesLaufs(laufId, { id: techniker.id, rolle: techniker.rolle })).toHaveLength(0);
|
||
} finally {
|
||
if (vorher === undefined) delete process.env["OIDC_CUSTOMER_GROUP_PREFIX"];
|
||
else process.env["OIDC_CUSTOMER_GROUP_PREFIX"] = vorher;
|
||
}
|
||
});
|
||
|
||
it("hält die Mandantengrenze in der Datenbank, nicht nur im Dienst", async () => {
|
||
const { laufId, schrittId, ergebnisId } = await aufbau();
|
||
const fremd = await testPrisma.kunde.create({ data: { name: "Fremd GmbH", kuerzel: "FREMD" } });
|
||
expect(schrittId).toBeTruthy();
|
||
|
||
// Ein Anhang mit fremdem Kunden, an der Service-Schicht vorbei: der
|
||
// zusammengesetzte Fremdschlüssel weist das ab.
|
||
await expect(
|
||
testPrisma.anhang.create({
|
||
data: {
|
||
schrittergebnisId: ergebnisId,
|
||
laufId,
|
||
kundeId: fremd.id,
|
||
dateiname: "x.png",
|
||
medientyp: "image/png",
|
||
groesse: 10,
|
||
pruefsumme: "a".repeat(64),
|
||
},
|
||
}),
|
||
).rejects.toThrow(/Anhang_lauf_gleicher_kunde/);
|
||
});
|
||
|
||
it("weist eine unmögliche Größe und eine falsche Prüfsumme ab", async () => {
|
||
const { laufId, ergebnisId, kunde } = await aufbau();
|
||
const basis = {
|
||
schrittergebnisId: ergebnisId,
|
||
laufId,
|
||
kundeId: kunde.id,
|
||
dateiname: "x.png",
|
||
medientyp: "image/png",
|
||
};
|
||
await expect(
|
||
testPrisma.anhang.create({ data: { ...basis, groesse: 0, pruefsumme: "a".repeat(64) } }),
|
||
).rejects.toThrow(/Anhang_groesse_positiv/);
|
||
await expect(
|
||
testPrisma.anhang.create({ data: { ...basis, groesse: 10, pruefsumme: "keine pruefsumme" } }),
|
||
).rejects.toThrow(/Anhang_pruefsumme_form/);
|
||
});
|
||
|
||
it("verweigert den Abruf für einen auf andere Kunden beschränkten Benutzer", async () => {
|
||
const vorher = process.env["OIDC_CUSTOMER_GROUP_PREFIX"];
|
||
process.env["OIDC_CUSTOMER_GROUP_PREFIX"] = "kunde-";
|
||
try {
|
||
const { admin, laufId, schrittId } = await aufbau();
|
||
await testPrisma.einstellung.update({
|
||
where: { id: "global" },
|
||
data: { modus: Betriebsmodus.DIENSTLEISTUNG },
|
||
});
|
||
const { id } = await anhangSpeichern(
|
||
{
|
||
laufId,
|
||
vorlagenSchrittId: schrittId,
|
||
dateiname: "a.png",
|
||
medientyp: "image/png",
|
||
inhalt: new Uint8Array(PNG),
|
||
},
|
||
admin,
|
||
);
|
||
|
||
const fremd = await testPrisma.kunde.create({ data: { name: "Fremd GmbH", kuerzel: "FREMD" } });
|
||
const techniker = await testPrisma.benutzer.create({
|
||
data: { email: "tech@firma.example", name: "Techniker", rolle: "TECHNIKER" },
|
||
});
|
||
await testPrisma.benutzerKunde.create({
|
||
data: { benutzerId: techniker.id, kundeId: fremd.id },
|
||
});
|
||
|
||
await expect(anhangLesen(id, { id: techniker.id, rolle: techniker.rolle })).rejects.toThrow(
|
||
MandantFehler,
|
||
);
|
||
} finally {
|
||
if (vorher === undefined) delete process.env["OIDC_CUSTOMER_GROUP_PREFIX"];
|
||
else process.env["OIDC_CUSTOMER_GROUP_PREFIX"] = vorher;
|
||
}
|
||
});
|
||
|
||
it("wirft bei einem Anhang, den es nicht gibt, einen eigenen Fehler", async () => {
|
||
const { admin } = await aufbau();
|
||
await expect(anhangLesen("gibt-es-nicht", admin)).rejects.toThrow(AnhangFehler);
|
||
await expect(anhangEntfernen("gibt-es-nicht", admin)).rejects.toThrow(AnhangFehler);
|
||
});
|
||
});
|