feat(anhaenge): Fotos und Dateien an einem Schritt (#52)
Ein Schritt nimmt jetzt Bilder (JPEG, PNG, WebP, HEIC) und PDF auf, am Handy direkt aus der Kamera. Die Dateien liegen im Dateisystem unter ANHAENGE_PFAD, nach Kunde und Lauf gegliedert; in der Datenbank stehen nur Name, Typ, Groesse und Pruefsumme. Der Pfad entsteht aus der Kennung, nie aus dem Dateinamen vom Browser. Jeder Abruf prueft Recht und Mandantengrenze — auch der des Bildes selbst: der Route Handler ruft rechtErforderlich() selbst, weil die Huelle unter (app) Route Handler nicht abdeckt. Hochgeladen wird ueber eine Server Action statt ueber einen zweiten Endpunkt; der Archiv-Schreibschutz haengt an derselben einen Stelle wie alles andere am Lauf. Kein SVG: ein SVG ist ein Dokument, das Skripte ausfuehrt. In der Datenbank halten zwei CHECK-Bedingungen und zwei zusammengesetzte Fremdschluessel fest, was auch gilt, wenn jemand an der Service-Schicht vorbei schreibt — ein Anhang kann strukturell nicht an einem Schritt eines anderen Laufs oder bei einem fremden Kunden haengen. docs/betrieb/sicherung.md ist neu geschrieben: dort stand "Kein Upload-Verzeichnis, keine erzeugten Dateien", und eine Sicherung nach der alten Anleitung haette ab hier still Daten verloren. Sicherung und Ruecksicherung sind einmal wirklich gefahren, mit Pruefsummenvergleich. Gefunden in der Klickprobe am gebauten Stand und nur dort: zwei Felder "datei" in einem Formular, und FormData.get nahm das leere. Wer "Datei" waehlte, bekam "Keine Datei gewaehlt", waehrend "Foto" ging. Die Regel liegt jetzt als reine Funktion in src/lib, und tests/e2e/anhaenge.spec.ts laedt bewusst ueber den zweiten Knopf hoch. Closes #52 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
855164066c
commit
3cbd5026f8
@@ -0,0 +1,486 @@
|
||||
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,
|
||||
anhaengeDesLaufs,
|
||||
anhangEntfernen,
|
||||
anhangLesen,
|
||||
anhangPfad,
|
||||
anhangSpeichern,
|
||||
} from "@/server/services/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);
|
||||
expect(liste).toHaveLength(1);
|
||||
expect(liste[0]).toMatchObject({
|
||||
schrittergebnisId: ergebnisId,
|
||||
dateiname: "eins.png",
|
||||
erstelltVon: "Admin",
|
||||
});
|
||||
});
|
||||
|
||||
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);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user