feat(tags): QR-Aufkleber, Geraeteseite, Etikettendruck und Scanner
Der Einstieg, fuer den die Anwendung gebaut ist: scannen, sehen was zu tun ist,
loslegen. `/t/<tagCode>` kennt drei Faelle — registriertes Geraet, vorgedruckter
Tag, unbekannter Code —, die Geraeteseite zeigt Zuweisungen, Schnellstart und
Historie, und beides ist am Handy bedienbar.
Im QR steht ausschliesslich `{APP_URL}/t/{tagCode}`: kein Name, keine
Seriennummer, kein Besitzer. Ein Aufkleber haengt jahrelang sichtbar am Geraet
und wird von jedem Telefon gelesen; was darauf steht, ist veroeffentlicht. Die
Masse sind gemessen, nicht behauptet: ein echtes PDF liefert 62 x 29 mm und A4,
und ein bei 300 dpi gerastertes Etikett wird mit demselben Decoder
zurueckgelesen, den auch der Scanner benutzt.
Drei Defekte zeigte erst der gebaute Stand, alle an den Naehten zwischen den
parallel gebauten Teilen: „Starten“ setzte IN_ARBEIT und sprang nicht, „Vorlage
zuweisen“ bot Vorlagen an, die der Dienst danach ablehnte, und die Geraeteliste
verlinkte nichts. Der unabhaengige Review fand drei Blocker — darunter, dass der
Scanner nie die native BarcodeDetector-API benutzte und dass ein Unterpfad in
APP_URL aus dem gedruckten QR herausfiel.
Closes #14
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit was merged in pull request #28.
This commit is contained in:
@@ -0,0 +1,567 @@
|
||||
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);
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user