import { beforeEach, describe, expect, it } from "vitest"; import { Betriebsmodus, GeraeteTyp, LaufStatus, Rolle, SchrittZustand, VersionStatus, } from "@/generated/prisma/enums"; import { MandantFehler } from "@/server/mandant"; import { GeraeteFehler, geraetAnlegen, geraetArchivieren, geraetBearbeiten, laufFuerGeraetAnlegen, } from "@/server/services/geraete"; import { LaufFehler, laufAbschliessen, laufArchivieren, laufZurBearbeitungOeffnen, } from "@/server/services/laeufe"; import { HISTORIE_HOECHSTENS, geraetHolen, geraetNachTagCode, laeufeDesGeraets, vorlagenFuerGeraet, } from "@/server/queries/geraete"; import { datenbankLeeren, datenbankVerfuegbar, testPrisma } from "./helpers/db"; /** * Die Geräteseite und der Einstieg über den Aufkleber (#014). * * Festgehalten wird, was ein Techniker vor dem Gerät erlebt: ein Code führt zum * Gerät oder zum Registrierformular, mehrere Zuweisungen stehen gleichzeitig an * einem Gerät, „Vorlage zuweisen“ erzeugt einen `OFFEN`en Lauf, und „Starten“ * macht `IN_ARBEIT` daraus, ohne dass der Lauf aus der Liste verschwindet. * * Dazu die Mandantengrenze, und zwar an jeder Tür: ein Gerät eines fremden Kunden * ist über den Tag-Code genauso unerreichbar wie über seine Kennung, und beide * antworten wie „gibt es nicht“. */ async function grundbestand(modus: Betriebsmodus = Betriebsmodus.INTERN) { const eigene = await testPrisma.kunde.create({ data: { name: "Eigene Organisation", kuerzel: "EIGEN", istEigeneOrganisation: true }, }); await testPrisma.einstellung.create({ data: { id: "global", modus } }); const benutzer = await testPrisma.benutzer.create({ data: { email: "admin@firma.example", name: "Administrator", rolle: Rolle.ADMIN }, }); return { eigene, admin: { id: benutzer.id, rolle: Rolle.ADMIN } }; } type VorlagenWunsch = { readonly name?: string; readonly kategorie?: string | null; readonly kundeId?: string | null; readonly status?: VersionStatus; readonly archiviert?: boolean; readonly schritte?: number; /** Standard sind Pflichtschritte – ohne sie lässt sich ein Lauf sofort abschließen. */ readonly pflicht?: boolean; readonly pflichtVariable?: boolean; /** Legt die Vorgabe in `GeraetetypVorlage` an – damit taucht sie im Schnellstart auf. */ readonly fuerTyp?: GeraeteTyp; readonly position?: number; }; /** Eine Vorlage mit einer Version und ein paar Pflichtschritten. */ async function vorlageAnlegen(wunsch: VorlagenWunsch = {}) { const vorlage = await testPrisma.vorlage.create({ data: { name: wunsch.name ?? "PC-Neuinstallation", kategorie: wunsch.kategorie ?? null, kundeId: wunsch.kundeId ?? null, archiviert: wunsch.archiviert ?? false, }, }); const status = wunsch.status ?? VersionStatus.VEROEFFENTLICHT; /* * Erst als Entwurf mit Abschnitten und Schritten, dann veroeffentlichen: eine * veroeffentlichte Version ist in der Datenbank unveraenderlich, ein Trigger * weist den nachtraeglichen Abschnitt ab. Genau die Regel, die #009 zugesagt hat. */ const version = await testPrisma.vorlagenVersion.create({ data: { vorlageId: vorlage.id, version: 1, status: VersionStatus.ENTWURF, abschnitte: { create: { titel: "Vorbereitung", position: 0, schritte: { create: Array.from({ length: wunsch.schritte ?? 2 }, (_, index) => ({ titel: `Schritt ${index + 1}`, position: index, pflicht: wunsch.pflicht ?? true, })), }, }, }, ...(wunsch.pflichtVariable ? { variablen: { create: { schluessel: "upn", beschriftung: "Anmeldename", pflicht: true, position: 0 }, }, } : {}), }, }); if (status === VersionStatus.VEROEFFENTLICHT) { await testPrisma.vorlagenVersion.update({ where: { id: version.id }, data: { status, veroeffentlichtAm: new Date() }, }); } if (wunsch.fuerTyp) { await testPrisma.geraetetypVorlage.create({ data: { geraeteTyp: wunsch.fuerTyp, vorlageId: vorlage.id, position: wunsch.position ?? 0 }, }); } return { vorlage, versionId: version.id }; } describe.runIf(datenbankVerfuegbar)("Geräteseite", () => { beforeEach(async () => { await datenbankLeeren(); delete process.env["OIDC_CUSTOMER_GROUP_PREFIX"]; }); describe("Einstieg über den Tag", () => { it("findet das Gerät zum Tag-Code, gleich wie er geschrieben steht", async () => { const { admin } = await grundbestand(); const geraet = await geraetAnlegen({ hostname: "PC-001", typ: GeraeteTyp.PC }, admin); // Abgetippt wird klein, und ein Handy schreibt von sich aus klein. expect((await geraetNachTagCode(geraet.tagCode, admin))?.id).toBe(geraet.id); expect((await geraetNachTagCode(geraet.tagCode.toLowerCase(), admin))?.id).toBe(geraet.id); expect((await geraetNachTagCode(` ${geraet.tagCode}\n`, admin))?.id).toBe(geraet.id); }); it("liefert einen vorgedruckten Tag mit leerer Registrierung", async () => { const { admin } = await grundbestand(); const tag = await geraetAnlegen({ registrieren: false }, admin); const zeile = await geraetNachTagCode(tag.tagCode, admin); expect(zeile?.registriertAm).toBeNull(); // Ohne Namen bleibt der Code selbst die Anzeige – leer ist die Zeile nie. expect(zeile?.anzeige).toBe(tag.tagCode); }); it("antwortet auf einen unbekannten Code wie auf einen falschen: mit nichts", async () => { const { admin } = await grundbestand(); await geraetAnlegen({ hostname: "PC-001" }, admin); expect(await geraetNachTagCode("LZ-ZZZZZZ", admin)).toBeUndefined(); expect(await geraetNachTagCode("Unsinn", admin)).toBeUndefined(); expect(await geraetNachTagCode("", admin)).toBeUndefined(); }); it("biegt verwechselbare Zeichen nicht auf ein vorhandenes Gerät", async () => { const { admin, eigene } = await grundbestand(); await testPrisma.geraet.create({ data: { kundeId: eigene.id, tagCode: "LZ-LLLLLL", hostname: "PC-LLL", registriertAm: new Date(), }, }); // Das Alphabet kennt kein I. Ein getipptes „I“ auf das „L“ zu biegen führte // auf ein anderes, vorhandenes Gerät – und wer davor steht, sähe es nie. expect(await geraetNachTagCode("LZ-IIIIII", admin)).toBeUndefined(); expect((await geraetNachTagCode("lz-llllll", admin))?.hostname).toBe("PC-LLL"); }); }); describe("Registrieren", () => { it("macht aus dem Aufkleber ein Gerät mit Typ, Namen und Besitzer", async () => { const { admin, eigene } = await grundbestand(); const tag = await geraetAnlegen({ registrieren: false }, admin); const person = await testPrisma.mitarbeiter.create({ data: { kundeId: eigene.id, vorname: "Ada", nachname: "Lovelace", anzeigename: "Ada Lovelace", abteilung: "Vertrieb", }, }); await geraetBearbeiten( tag.id, { registrieren: true, typ: GeraeteTyp.NOTEBOOK, hostname: "NB-ADA", seriennummer: "SN-4711", mitarbeiterId: person.id, }, admin, ); const zeile = await geraetNachTagCode(tag.tagCode, admin); expect(zeile?.registriertAm).not.toBeNull(); expect(zeile?.anzeige).toBe("NB-ADA"); expect(zeile?.typ).toBe(GeraeteTyp.NOTEBOOK); expect(zeile?.besitzerName).toBe("Ada Lovelace"); // Die Abteilung steht im Kopf der Geräteseite neben dem Besitzer. expect(zeile?.besitzerAbteilung).toBe("Vertrieb"); // Der Tag-Code ändert sich dabei nicht. expect(zeile?.tagCode).toBe(tag.tagCode); }); it("lässt einen Tag unregistriert, wenn kein Name eingetragen wurde", async () => { const { admin } = await grundbestand(); const tag = await geraetAnlegen({ registrieren: false }, admin); await expect( geraetBearbeiten(tag.id, { registrieren: true, typ: GeraeteTyp.PC }, admin), ).rejects.toThrow(GeraeteFehler); expect((await geraetNachTagCode(tag.tagCode, admin))?.registriertAm).toBeNull(); }); }); describe("Zugewiesen und Historie", () => { it("zeigt mehrere Zuweisungen an einem Gerät gleichzeitig", async () => { const { admin } = await grundbestand(); const geraet = await geraetAnlegen({ hostname: "PC-001", typ: GeraeteTyp.PC }, admin); const ersteVorlage = await vorlageAnlegen({ name: "Patchday" }); const zweiteVorlage = await vorlageAnlegen({ name: "Rechnerwechsel" }); await laufFuerGeraetAnlegen({ geraetId: geraet.id, vorlagenVersionId: ersteVorlage.versionId }, admin); await laufFuerGeraetAnlegen({ geraetId: geraet.id, vorlagenVersionId: zweiteVorlage.versionId }, admin); const laeufe = await laeufeDesGeraets(geraet.id, admin); expect(laeufe.zugewiesen).toHaveLength(2); expect(laeufe.zugewiesen.every((zeile) => zeile.status === LaufStatus.OFFEN)).toBe(true); expect(laeufe.historie).toHaveLength(0); }); it("zeigt den Fortschritt: erledigt und nicht zutreffend zählen, fehlgeschlagen nicht", async () => { const { admin } = await grundbestand(); const geraet = await geraetAnlegen({ hostname: "PC-001" }, admin); const { versionId } = await vorlageAnlegen({ schritte: 4 }); const lauf = await laufFuerGeraetAnlegen({ geraetId: geraet.id, vorlagenVersionId: versionId }, admin); const ergebnisse = await testPrisma.schrittergebnis.findMany({ where: { laufId: lauf.id }, orderBy: { vorlagenSchrittId: "asc" }, }); const zustaende = [ SchrittZustand.ERLEDIGT, SchrittZustand.NICHT_ZUTREFFEND, SchrittZustand.FEHLGESCHLAGEN, SchrittZustand.OFFEN, ]; for (const [index, ergebnis] of ergebnisse.entries()) { await testPrisma.schrittergebnis.update({ where: { id: ergebnis.id }, data: { zustand: zustaende[index] ?? SchrittZustand.OFFEN, notiz: "Grund" }, }); } const laeufe = await laeufeDesGeraets(geraet.id, admin); expect(laeufe.zugewiesen[0]?.anzahlSchritte).toBe(4); expect(laeufe.zugewiesen[0]?.anzahlErledigt).toBe(2); }); it("trennt Abgeschlossenes und Archiviertes von der Arbeit, die wartet", async () => { const { admin } = await grundbestand(); const geraet = await geraetAnlegen({ hostname: "PC-001" }, admin); // Ohne Pflichtschritte lässt sich ein Lauf sofort abschließen; geprüft wird // hier die Aufteilung des Lesemodells, nicht die Abschlussbedingung. const { versionId } = await vorlageAnlegen({ pflicht: false }); const wartend = await laufFuerGeraetAnlegen( { geraetId: geraet.id, vorlagenVersionId: versionId }, admin, ); const fertig = await laufFuerGeraetAnlegen( { geraetId: geraet.id, vorlagenVersionId: versionId }, admin, ); const weggelegt = await laufFuerGeraetAnlegen( { geraetId: geraet.id, vorlagenVersionId: versionId }, admin, ); // Über die Dienste, nicht an ihnen vorbei: Abschluss und Archiv setzen // Zeitstempel, die die Datenbank auch verlangt. await laufAbschliessen(fertig.id, admin); await laufAbschliessen(weggelegt.id, admin); await laufArchivieren(weggelegt.id, admin); const laeufe = await laeufeDesGeraets(geraet.id, admin); expect(laeufe.zugewiesen.map((zeile) => zeile.id)).toEqual([wartend.id]); expect(laeufe.historie.map((zeile) => zeile.id).sort()).toEqual([fertig.id, weggelegt.id].sort()); expect(laeufe.historieGekuerzt).toBe(false); }); it("kappt die Historie und sagt, dass sie gekappt ist", async () => { const { admin, eigene } = await grundbestand(); const geraet = await geraetAnlegen({ hostname: "PC-001" }, admin); const { versionId } = await vorlageAnlegen(); // Direkt geschrieben: geprüft wird die Kappung des Lesemodells, nicht das // Anlegen – und Zuweisungen bleiben bewusst ungekappt. await testPrisma.lauf.createMany({ data: Array.from({ length: HISTORIE_HOECHSTENS + 3 }, (_, index) => ({ vorlagenVersionId: versionId, titel: `Alter Lauf ${index + 1}`, kundeId: eigene.id, geraetId: geraet.id, status: LaufStatus.ABGESCHLOSSEN, // Die Datenbank verlangt beide Zeitstempel, sobald ein Lauf nicht mehr // offen ist (CHECK `Lauf_begonnen_mit_zeitpunkt`). gestartetAm: new Date(2026, 0, index + 1), abgeschlossenAm: new Date(2026, 0, index + 1), })), }); const laeufe = await laeufeDesGeraets(geraet.id, admin); expect(laeufe.historie).toHaveLength(HISTORIE_HOECHSTENS); expect(laeufe.historieGekuerzt).toBe(true); // Das Jüngste zuerst – die Historie liest sich von oben. expect(laeufe.historie[0]?.titel).toBe(`Alter Lauf ${HISTORIE_HOECHSTENS + 3}`); }); }); describe("Vorlage zuweisen", () => { it("erzeugt einen Lauf in OFFEN, der unter „Zugewiesen“ erscheint", async () => { const { admin, eigene } = await grundbestand(); const person = await testPrisma.mitarbeiter.create({ data: { kundeId: eigene.id, vorname: "Ada", nachname: "Lovelace", anzeigename: "Ada Lovelace" }, }); const geraet = await geraetAnlegen( { hostname: "NB-ADA", typ: GeraeteTyp.NOTEBOOK, mitarbeiterId: person.id }, admin, ); const { versionId } = await vorlageAnlegen({ name: "Rechnerwechsel" }); const faelligAm = new Date(2026, 9, 30); const lauf = await laufFuerGeraetAnlegen( { geraetId: geraet.id, vorlagenVersionId: versionId, zugewiesenAnId: admin.id, faelligAm }, admin, ); expect(lauf.status).toBe(LaufStatus.OFFEN); expect(lauf.gestartetAm).toBeNull(); expect(lauf.geraetId).toBe(geraet.id); // Kunde und Besitzer kommen vom Gerät, nicht aus dem Formular. expect(lauf.kundeId).toBe(eigene.id); expect(lauf.mitarbeiterId).toBe(person.id); expect(lauf.zugewiesenAnId).toBe(admin.id); expect(lauf.faelligAm?.getTime()).toBe(faelligAm.getTime()); // Der Titel kommt aus Vorlage, Gerät und Datum – nicht aus der Oberfläche. expect(lauf.titel).toContain("Rechnerwechsel"); expect(lauf.titel).toContain("NB-ADA"); const laeufe = await laeufeDesGeraets(geraet.id, admin); expect(laeufe.zugewiesen.map((zeile) => zeile.id)).toEqual([lauf.id]); expect(laeufe.zugewiesen[0]?.zugewiesenAn).toBe("Administrator"); expect(laeufe.zugewiesen[0]?.anzahlSchritte).toBe(2); }); it("„Starten“ setzt IN_ARBEIT, ohne den Lauf aus der Liste zu nehmen", async () => { const { admin } = await grundbestand(); const geraet = await geraetAnlegen({ hostname: "PC-001" }, admin); const { versionId } = await vorlageAnlegen(); const lauf = await laufFuerGeraetAnlegen({ geraetId: geraet.id, vorlagenVersionId: versionId }, admin); const gestartet = await laufZurBearbeitungOeffnen(lauf.id, admin); expect(gestartet.status).toBe(LaufStatus.IN_ARBEIT); expect(gestartet.gestartetAm).not.toBeNull(); const laeufe = await laeufeDesGeraets(geraet.id, admin); expect(laeufe.zugewiesen.map((zeile) => zeile.status)).toEqual([LaufStatus.IN_ARBEIT]); expect(laeufe.historie).toHaveLength(0); }); it("weist eine Vorlage mit Pflichtangaben ab, statt sie halb zu starten", async () => { const { admin } = await grundbestand(); const geraet = await geraetAnlegen({ hostname: "PC-001" }, admin); const { versionId } = await vorlageAnlegen({ pflichtVariable: true }); // Genau deshalb führt der Schnellstart solche Vorlagen in den Start-Dialog: // dort kann jemand die Angabe machen. await expect( laufFuerGeraetAnlegen({ geraetId: geraet.id, vorlagenVersionId: versionId }, admin), ).rejects.toThrow(/Diese Angaben fehlen/u); expect(await testPrisma.lauf.count()).toBe(0); }); it("nimmt an einem archivierten Gerät keine neue Arbeit auf", async () => { const { admin } = await grundbestand(); const geraet = await geraetAnlegen({ hostname: "PC-001" }, admin); const { versionId } = await vorlageAnlegen(); await geraetArchivieren(geraet.id, true, admin); await expect( laufFuerGeraetAnlegen({ geraetId: geraet.id, vorlagenVersionId: versionId }, admin), ).rejects.toThrow(LaufFehler); }); it("weist einen Entwurf ab – ein Lauf hängt nur an einer veröffentlichten Version", async () => { const { admin } = await grundbestand(); const geraet = await geraetAnlegen({ hostname: "PC-001" }, admin); const { versionId } = await vorlageAnlegen({ status: VersionStatus.ENTWURF }); await expect( laufFuerGeraetAnlegen({ geraetId: geraet.id, vorlagenVersionId: versionId }, admin), ).rejects.toThrow(LaufFehler); }); }); describe("Schnellstart und Vorlagenauswahl", () => { it("zeigt im Schnellstart nur die Vorgaben dieses Gerätetyps, in ihrer Reihenfolge", async () => { const { admin } = await grundbestand(); const geraet = await geraetAnlegen({ hostname: "PC-001", typ: GeraeteTyp.PC }, admin); await vorlageAnlegen({ name: "Zweiter Griff", fuerTyp: GeraeteTyp.PC, position: 1 }); await vorlageAnlegen({ name: "Erster Griff", fuerTyp: GeraeteTyp.PC, position: 0 }); await vorlageAnlegen({ name: "Nur für Server", fuerTyp: GeraeteTyp.SERVER }); await vorlageAnlegen({ name: "Ohne Vorgabe" }); const vorlagen = await vorlagenFuerGeraet(geraet, admin); expect(vorlagen.schnellstart.map((eintrag) => eintrag.name)).toEqual(["Erster Griff", "Zweiter Griff"]); // „Vorlage zuweisen“ bietet alles an, was startbar ist. expect(vorlagen.alle.map((eintrag) => eintrag.name).sort()).toEqual([ "Erster Griff", "Nur für Server", "Ohne Vorgabe", "Zweiter Griff", ]); }); it("nimmt weder Entwürfe noch archivierte Vorlagen", async () => { const { admin } = await grundbestand(); const geraet = await geraetAnlegen({ hostname: "PC-001", typ: GeraeteTyp.PC }, admin); await vorlageAnlegen({ name: "Fertig", fuerTyp: GeraeteTyp.PC }); await vorlageAnlegen({ name: "Noch Entwurf", fuerTyp: GeraeteTyp.PC, status: VersionStatus.ENTWURF }); await vorlageAnlegen({ name: "Weggelegt", fuerTyp: GeraeteTyp.PC, archiviert: true }); const vorlagen = await vorlagenFuerGeraet(geraet, admin); expect(vorlagen.schnellstart.map((eintrag) => eintrag.name)).toEqual(["Fertig"]); expect(vorlagen.alle.map((eintrag) => eintrag.name)).toEqual(["Fertig"]); }); it("merkt an einer Vorlage, dass sie Pflichtangaben verlangt", async () => { const { admin } = await grundbestand(); const geraet = await geraetAnlegen({ hostname: "PC-001", typ: GeraeteTyp.PC }, admin); await vorlageAnlegen({ name: "Mit Frage", fuerTyp: GeraeteTyp.PC, pflichtVariable: true }); await vorlageAnlegen({ name: "Ohne Frage", fuerTyp: GeraeteTyp.PC, position: 1 }); const vorlagen = await vorlagenFuerGeraet(geraet, admin); const mitFrage = vorlagen.schnellstart.find((eintrag) => eintrag.name === "Mit Frage"); const ohneFrage = vorlagen.schnellstart.find((eintrag) => eintrag.name === "Ohne Frage"); expect(mitFrage?.pflichtangaben).toBe(true); expect(ohneFrage?.pflichtangaben).toBe(false); }); }); /** * Die Mandantengrenze an jeder Tür der Geräteseite. Ein fremdes Gerät ist über * den Tag-Code genauso unerreichbar wie über seine Kennung — und beide * antworten, als gäbe es es nicht. */ describe("Mandantensicht", () => { async function zweiKunden() { await grundbestand(Betriebsmodus.DIENSTLEISTUNG); process.env["OIDC_CUSTOMER_GROUP_PREFIX"] = "lz-kunde-"; const meiner = await testPrisma.kunde.create({ data: { name: "Meiner", kuerzel: "MEIN" } }); const fremder = await testPrisma.kunde.create({ data: { name: "Fremder", kuerzel: "FREMD" } }); const benutzer = await testPrisma.benutzer.create({ data: { email: "t@firma.example", name: "Technikerin", rolle: Rolle.TECHNIKER }, }); await testPrisma.benutzerKunde.create({ data: { benutzerId: benutzer.id, kundeId: meiner.id } }); const meinGeraet = await testPrisma.geraet.create({ data: { kundeId: meiner.id, tagCode: "LZ-AAAAAA", hostname: "PC-MEIN", typ: GeraeteTyp.PC, registriertAm: new Date(), }, }); const fremdesGeraet = await testPrisma.geraet.create({ data: { kundeId: fremder.id, tagCode: "LZ-BBBBBB", hostname: "PC-FREMD", typ: GeraeteTyp.PC, registriertAm: new Date(), }, }); return { meiner, fremder, meinGeraet, fremdesGeraet, technikerin: { id: benutzer.id, rolle: Rolle.TECHNIKER }, }; } it("beantwortet ein fremdes Gerät über Tag-Code und Kennung wie ein unbekanntes", async () => { const { fremdesGeraet, meinGeraet, technikerin } = await zweiKunden(); expect(await geraetNachTagCode("LZ-BBBBBB", technikerin)).toBeUndefined(); expect(await geraetHolen(fremdesGeraet.id, technikerin)).toBeUndefined(); // Das eigene bleibt erreichbar – sonst prüfte der Test nur eine leere Sicht. expect((await geraetNachTagCode("lz-aaaaaa", technikerin))?.id).toBe(meinGeraet.id); }); it("zeigt die Läufe eines fremden Geräts nicht", async () => { const { fremder, fremdesGeraet, technikerin } = await zweiKunden(); const { versionId } = await vorlageAnlegen(); await testPrisma.lauf.create({ data: { vorlagenVersionId: versionId, titel: "Geheime Arbeit", kundeId: fremder.id, geraetId: fremdesGeraet.id, }, }); const laeufe = await laeufeDesGeraets(fremdesGeraet.id, technikerin); expect(laeufe.zugewiesen).toHaveLength(0); expect(laeufe.historie).toHaveLength(0); }); it("bietet zu einem fremden Gerät keine Vorlagen an", async () => { const { fremder, technikerin } = await zweiKunden(); await vorlageAnlegen({ name: "Für alle", fuerTyp: GeraeteTyp.PC }); const vorlagen = await vorlagenFuerGeraet({ typ: GeraeteTyp.PC, kundeId: fremder.id }, technikerin); expect(vorlagen.schnellstart).toHaveLength(0); expect(vorlagen.alle).toHaveLength(0); }); it("bietet an einem Gerät nur Vorlagen an, die zu seinem Kunden gehören", async () => { const { meiner, fremder, meinGeraet, technikerin } = await zweiKunden(); await vorlageAnlegen({ name: "Für alle" }); await vorlageAnlegen({ name: "Nur für meinen", kundeId: meiner.id }); await vorlageAnlegen({ name: "Nur für fremde", kundeId: fremder.id }); const vorlagen = await vorlagenFuerGeraet(meinGeraet, technikerin); expect(vorlagen.alle.map((eintrag) => eintrag.name).sort()).toEqual(["Für alle", "Nur für meinen"]); }); it("lässt an einem fremden Gerät keinen Lauf anlegen", async () => { const { fremdesGeraet, technikerin } = await zweiKunden(); const { versionId } = await vorlageAnlegen(); await expect( laufFuerGeraetAnlegen({ geraetId: fremdesGeraet.id, vorlagenVersionId: versionId }, technikerin), ).rejects.toThrow(MandantFehler); expect(await testPrisma.lauf.count()).toBe(0); }); }); });