feat(schnellstart): Kacheln je Geraetetyp in der Oberflaeche pflegen (#44)
This commit was merged in pull request #44.
This commit is contained in:
@@ -0,0 +1,264 @@
|
||||
import { beforeEach, describe, expect, it } from "vitest";
|
||||
import { Betriebsmodus, GeraeteTyp, VersionStatus } from "@/generated/prisma/enums";
|
||||
import { RECHTE } from "@/server/auth/rollen";
|
||||
import { schnellstartPflege } from "@/server/queries/schnellstart";
|
||||
import { SchnellstartFehler, schnellstartSetzen } from "@/server/services/schnellstart";
|
||||
import { datenbankLeeren, datenbankVerfuegbar, testPrisma } from "./helpers/db";
|
||||
|
||||
/**
|
||||
* Schnellstart je Geraetetyp pflegen (#022).
|
||||
*
|
||||
* Dass die Kacheln auf der Geraeteseite richtig erscheinen, prueft
|
||||
* `tests/geraeteseite.test.ts` — auch den Fall, dass eine archivierte Vorlage von
|
||||
* selbst verschwindet. Hier geht es um das Pflegen: die Reihenfolge, die
|
||||
* Abweisungen, das Protokoll, und die Marke, die den Seed heraushaelt.
|
||||
*/
|
||||
|
||||
async function grundbestand() {
|
||||
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" },
|
||||
});
|
||||
return { kunde, adminId: admin.id };
|
||||
}
|
||||
|
||||
/** Eine Vorlage mit veroeffentlichter Version 1 – nur die ist zuweisbar. */
|
||||
async function vorlageAnlegen(
|
||||
name: string,
|
||||
optionen: { archiviert?: boolean; nurEntwurf?: boolean; kundeId?: string } = {},
|
||||
): Promise<string> {
|
||||
const vorlage = await testPrisma.vorlage.create({
|
||||
data: {
|
||||
name,
|
||||
archiviert: optionen.archiviert ?? false,
|
||||
...(optionen.kundeId ? { kundeId: optionen.kundeId } : {}),
|
||||
},
|
||||
});
|
||||
const version = await testPrisma.vorlagenVersion.create({
|
||||
data: { vorlageId: vorlage.id, version: 1, status: VersionStatus.ENTWURF },
|
||||
});
|
||||
if (!optionen.nurEntwurf) {
|
||||
await testPrisma.vorlagenVersion.update({
|
||||
where: { id: version.id },
|
||||
data: { status: VersionStatus.VEROEFFENTLICHT, veroeffentlichtAm: new Date() },
|
||||
});
|
||||
}
|
||||
return vorlage.id;
|
||||
}
|
||||
|
||||
async function listeVon(typ: GeraeteTyp): Promise<readonly string[]> {
|
||||
const { typen } = await schnellstartPflege();
|
||||
return typen.find((eintrag) => eintrag.typ === typ)?.zugewiesen.map((e) => e.name) ?? [];
|
||||
}
|
||||
|
||||
describe("Recht auf den Schnellstart", () => {
|
||||
it("liegt bei verwaltung, also nur bei ADMIN", () => {
|
||||
// Die Zuordnung gilt global und bestimmt, was jeder Techniker an jedem Gerät
|
||||
// zuerst sieht. TECHNIKER und LESER dürfen das nicht ändern.
|
||||
expect(RECHTE.verwaltung).toEqual(["ADMIN"]);
|
||||
});
|
||||
});
|
||||
|
||||
describe.runIf(datenbankVerfuegbar)("Schnellstart pflegen", () => {
|
||||
beforeEach(async () => {
|
||||
await datenbankLeeren();
|
||||
});
|
||||
|
||||
it("setzt die Liste in der übergebenen Reihenfolge", async () => {
|
||||
const { adminId } = await grundbestand();
|
||||
const erste = await vorlageAnlegen("Erster Griff");
|
||||
const zweite = await vorlageAnlegen("Zweiter Griff");
|
||||
|
||||
await schnellstartSetzen(GeraeteTyp.PC, [zweite, erste], adminId);
|
||||
expect(await listeVon(GeraeteTyp.PC)).toEqual(["Zweiter Griff", "Erster Griff"]);
|
||||
|
||||
// Umsortieren heißt ersetzen, nicht ergänzen.
|
||||
await schnellstartSetzen(GeraeteTyp.PC, [erste, zweite], adminId);
|
||||
expect(await listeVon(GeraeteTyp.PC)).toEqual(["Erster Griff", "Zweiter Griff"]);
|
||||
expect(await testPrisma.geraetetypVorlage.count({ where: { geraeteTyp: GeraeteTyp.PC } })).toBe(2);
|
||||
});
|
||||
|
||||
it("sortiert auch um, wenn alte Zeilen dieselbe Position tragen", async () => {
|
||||
/*
|
||||
* Der Fall, den erst das Durchklicken des gebauten Stands zeigte. Altdaten
|
||||
* tragen mehrere Zeilen auf `position` 0 — der Seed zaehlt je Vorgabe von 0,
|
||||
* und ein Beispielskript legte eine zweite dazu. Bei gleicher Position ist
|
||||
* die Reihenfolge in Postgres nicht festgelegt, und der Vergleich
|
||||
* "unveraendert" hielt eine echte Umsortierung deshalb fuer einen Leerlauf:
|
||||
* die Marke wurde gesetzt, geschrieben wurde nichts.
|
||||
*/
|
||||
const { adminId } = await grundbestand();
|
||||
const a = await vorlageAnlegen("Alpha");
|
||||
const b = await vorlageAnlegen("Beta");
|
||||
await testPrisma.geraetetypVorlage.createMany({
|
||||
data: [
|
||||
{ geraeteTyp: GeraeteTyp.PC, vorlageId: a, position: 0 },
|
||||
{ geraeteTyp: GeraeteTyp.PC, vorlageId: b, position: 0 },
|
||||
],
|
||||
});
|
||||
|
||||
// Genau die Reihenfolge schicken, die das Lesemodell zeigt – ein Mensch, der
|
||||
// nichts umsortiert, aber auf Speichern drückt.
|
||||
const gezeigt = await listeVon(GeraeteTyp.PC);
|
||||
const nachName = new Map([
|
||||
["Alpha", a],
|
||||
["Beta", b],
|
||||
]);
|
||||
const umgekehrt = [...gezeigt].reverse().map((name) => nachName.get(name) ?? "");
|
||||
await schnellstartSetzen(GeraeteTyp.PC, umgekehrt, adminId);
|
||||
|
||||
expect(await listeVon(GeraeteTyp.PC)).toEqual([...gezeigt].reverse());
|
||||
// Und die Positionen stehen danach aufeinanderfolgend, nicht beide auf 0.
|
||||
const zeilen = await testPrisma.geraetetypVorlage.findMany({
|
||||
where: { geraeteTyp: GeraeteTyp.PC },
|
||||
orderBy: { position: "asc" },
|
||||
select: { position: true },
|
||||
});
|
||||
expect(zeilen.map((zeile) => zeile.position)).toEqual([0, 1]);
|
||||
});
|
||||
|
||||
it("zieht beim Speichern alte, gleiche Positionen gerade", async () => {
|
||||
// Ohne Aenderung gespeichert: die Liste bleibt, die Positionen werden
|
||||
// aufeinanderfolgend. Aufraeumen, kein Fehlerfall (#022).
|
||||
const { adminId } = await grundbestand();
|
||||
const a = await vorlageAnlegen("Alpha");
|
||||
const b = await vorlageAnlegen("Beta");
|
||||
await testPrisma.geraetetypVorlage.createMany({
|
||||
data: [
|
||||
{ geraeteTyp: GeraeteTyp.PC, vorlageId: a, position: 0 },
|
||||
{ geraeteTyp: GeraeteTyp.PC, vorlageId: b, position: 0 },
|
||||
],
|
||||
});
|
||||
|
||||
const gezeigt = await listeVon(GeraeteTyp.PC);
|
||||
const nachName = new Map([
|
||||
["Alpha", a],
|
||||
["Beta", b],
|
||||
]);
|
||||
await schnellstartSetzen(
|
||||
GeraeteTyp.PC,
|
||||
gezeigt.map((name) => nachName.get(name) ?? ""),
|
||||
adminId,
|
||||
);
|
||||
|
||||
expect(await listeVon(GeraeteTyp.PC)).toEqual(gezeigt);
|
||||
const zeilen = await testPrisma.geraetetypVorlage.findMany({
|
||||
where: { geraeteTyp: GeraeteTyp.PC },
|
||||
orderBy: { position: "asc" },
|
||||
select: { position: true },
|
||||
});
|
||||
expect(zeilen.map((zeile) => zeile.position)).toEqual([0, 1]);
|
||||
});
|
||||
|
||||
it("nimmt eine leere Liste als Angabe und entfernt alles", async () => {
|
||||
const { adminId } = await grundbestand();
|
||||
const vorlage = await vorlageAnlegen("Einziger Griff");
|
||||
await schnellstartSetzen(GeraeteTyp.PC, [vorlage], adminId);
|
||||
|
||||
await schnellstartSetzen(GeraeteTyp.PC, [], adminId);
|
||||
expect(await listeVon(GeraeteTyp.PC)).toEqual([]);
|
||||
});
|
||||
|
||||
it("lässt andere Gerätetypen unberührt", async () => {
|
||||
const { adminId } = await grundbestand();
|
||||
const vorlage = await vorlageAnlegen("Für beide");
|
||||
await schnellstartSetzen(GeraeteTyp.PC, [vorlage], adminId);
|
||||
await schnellstartSetzen(GeraeteTyp.SERVER, [vorlage], adminId);
|
||||
|
||||
await schnellstartSetzen(GeraeteTyp.PC, [], adminId);
|
||||
expect(await listeVon(GeraeteTyp.SERVER)).toEqual(["Für beide"]);
|
||||
});
|
||||
|
||||
it("weist dieselbe Vorlage zweimal in einer Liste ab", async () => {
|
||||
const { adminId } = await grundbestand();
|
||||
const vorlage = await vorlageAnlegen("Einmal reicht");
|
||||
await expect(schnellstartSetzen(GeraeteTyp.PC, [vorlage, vorlage], adminId)).rejects.toThrow(
|
||||
SchnellstartFehler,
|
||||
);
|
||||
});
|
||||
|
||||
it("weist eine archivierte Vorlage ab", async () => {
|
||||
const { adminId } = await grundbestand();
|
||||
const weggelegt = await vorlageAnlegen("Weggelegt", { archiviert: true });
|
||||
await expect(schnellstartSetzen(GeraeteTyp.PC, [weggelegt], adminId)).rejects.toThrow(/archiviert/);
|
||||
});
|
||||
|
||||
it("weist eine Vorlage ohne veröffentlichte Version ab", async () => {
|
||||
// Eine Kachel, die zu einem Entwurf führt, kann keinen Lauf starten.
|
||||
const { adminId } = await grundbestand();
|
||||
const entwurf = await vorlageAnlegen("Noch Entwurf", { nurEntwurf: true });
|
||||
await expect(schnellstartSetzen(GeraeteTyp.PC, [entwurf], adminId)).rejects.toThrow(
|
||||
/veröffentlichte Version/,
|
||||
);
|
||||
});
|
||||
|
||||
it("bietet zur Auswahl nur veröffentlichte, nicht archivierte Vorlagen", async () => {
|
||||
const { kunde } = await grundbestand();
|
||||
await vorlageAnlegen("Brauchbar");
|
||||
await vorlageAnlegen("Weggelegt", { archiviert: true });
|
||||
await vorlageAnlegen("Noch Entwurf", { nurEntwurf: true });
|
||||
await vorlageAnlegen("Nur für einen Kunden", { kundeId: kunde.id });
|
||||
|
||||
const { auswahl } = await schnellstartPflege();
|
||||
expect(auswahl.map((eintrag) => eintrag.name).sort()).toEqual(["Brauchbar", "Nur für einen Kunden"]);
|
||||
// Eine kundengebundene Vorlage ist zuweisbar und wird als solche gezeigt;
|
||||
// dass sie nur bei ihren Geräten erscheint, entscheidet das Lesen.
|
||||
expect(auswahl.find((eintrag) => eintrag.name === "Nur für einen Kunden")?.kundeName).toBe(
|
||||
"Eigene Organisation",
|
||||
);
|
||||
});
|
||||
|
||||
it("nennt jeden Gerätetyp, auch die ohne Zuordnung", async () => {
|
||||
await grundbestand();
|
||||
const { typen } = await schnellstartPflege();
|
||||
expect(typen.map((eintrag) => eintrag.typ)).toEqual(Object.values(GeraeteTyp));
|
||||
});
|
||||
|
||||
it("schreibt die Änderung mit Namen ins Protokoll, nicht mit Kennungen", async () => {
|
||||
const { adminId } = await grundbestand();
|
||||
const erste = await vorlageAnlegen("Erster Griff");
|
||||
const zweite = await vorlageAnlegen("Zweiter Griff");
|
||||
await schnellstartSetzen(GeraeteTyp.PC, [erste], adminId);
|
||||
await schnellstartSetzen(GeraeteTyp.PC, [erste, zweite], adminId);
|
||||
|
||||
const eintraege = await testPrisma.protokoll.findMany({
|
||||
where: { entitaet: "GeraetetypVorlage" },
|
||||
orderBy: { erstelltAm: "asc" },
|
||||
});
|
||||
expect(eintraege).toHaveLength(2);
|
||||
expect(eintraege[1]?.aktion).toBe("Schnellstart für PC geändert");
|
||||
expect(eintraege[1]?.vorher).toEqual({ vorlagen: ["Erster Griff"] });
|
||||
expect(eintraege[1]?.nachher).toEqual({ vorlagen: ["Erster Griff", "Zweiter Griff"] });
|
||||
// Ein Protokolleintrag muss auch lesbar sein, wenn die Vorlage später
|
||||
// anders heißt – deshalb Namen und keine Kennungen.
|
||||
expect(JSON.stringify(eintraege[1]?.nachher)).not.toContain(erste);
|
||||
});
|
||||
|
||||
it("schreibt keinen Protokolleintrag, wenn sich die Liste nicht ändert", async () => {
|
||||
const { adminId } = await grundbestand();
|
||||
const vorlage = await vorlageAnlegen("Unverändert");
|
||||
await schnellstartSetzen(GeraeteTyp.PC, [vorlage], adminId);
|
||||
await schnellstartSetzen(GeraeteTyp.PC, [vorlage], adminId);
|
||||
|
||||
expect(await testPrisma.protokoll.count({ where: { entitaet: "GeraetetypVorlage" } })).toBe(1);
|
||||
});
|
||||
|
||||
it("setzt die Marke, die den Seed heraushält – auch bei unveränderter Liste", async () => {
|
||||
const { adminId } = await grundbestand();
|
||||
const vorlage = await vorlageAnlegen("Einziger Griff");
|
||||
expect(
|
||||
(await testPrisma.einstellung.findUniqueOrThrow({ where: { id: "global" } })).schnellstartGepflegt,
|
||||
).toBe(false);
|
||||
|
||||
// Ein Speichern, das nichts ändert, ist trotzdem die Aussage „ich pflege das
|
||||
// selbst“ — sonst legte der Seed seine Vorgaben weiter nach.
|
||||
await schnellstartSetzen(GeraeteTyp.PC, [], adminId);
|
||||
expect(
|
||||
(await testPrisma.einstellung.findUniqueOrThrow({ where: { id: "global" } })).schnellstartGepflegt,
|
||||
).toBe(true);
|
||||
expect(vorlage).toBeTruthy();
|
||||
});
|
||||
});
|
||||
@@ -414,6 +414,36 @@ describe.runIf(datenbankVerfuegbar)("Seed gegen die Datenbank", () => {
|
||||
expect(await typVorgabenSaeen(testPrisma, gesaet)).toBe(0);
|
||||
});
|
||||
|
||||
it("laesst den Schnellstart in Ruhe, sobald er in der Oberflaeche gepflegt wurde", async () => {
|
||||
// Der Fall aus #022: wer eine Kachel entfernt, soll sie nicht beim naechsten
|
||||
// Seed-Lauf zurueckbekommen. Eine fehlende Zeile sagt nicht, ob sie noch nie
|
||||
// da war oder bewusst entfernt wurde — die Marke sagt es.
|
||||
const gesaet = await vorlagenSaeen(testPrisma);
|
||||
expect(await typVorgabenSaeen(testPrisma, gesaet)).toBeGreaterThan(0);
|
||||
|
||||
await testPrisma.geraetetypVorlage.deleteMany({ where: { geraeteTyp: GeraeteTyp.PC } });
|
||||
// upsert, weil dieser Block die Einstellung nicht selbst anlegt - und genau
|
||||
// deshalb belegt der Test auch, dass typVorgabenSaeen ohne sie zurechtkommt.
|
||||
await testPrisma.einstellung.upsert({
|
||||
where: { id: "global" },
|
||||
create: { id: "global", schnellstartGepflegt: true },
|
||||
update: { schnellstartGepflegt: true },
|
||||
});
|
||||
|
||||
expect(await typVorgabenSaeen(testPrisma, gesaet)).toBe(0);
|
||||
expect(await testPrisma.geraetetypVorlage.count({ where: { geraeteTyp: GeraeteTyp.PC } })).toBe(0);
|
||||
});
|
||||
|
||||
it("legt die Vorgaben weiter an, solange niemand sie gepflegt hat", async () => {
|
||||
const gesaet = await vorlagenSaeen(testPrisma);
|
||||
await typVorgabenSaeen(testPrisma, gesaet);
|
||||
const vorher = await testPrisma.geraetetypVorlage.count({ where: { geraeteTyp: GeraeteTyp.PC } });
|
||||
expect(vorher).toBeGreaterThan(0);
|
||||
|
||||
await testPrisma.geraetetypVorlage.deleteMany({ where: { geraeteTyp: GeraeteTyp.PC } });
|
||||
expect(await typVorgabenSaeen(testPrisma, gesaet)).toBe(vorher);
|
||||
});
|
||||
|
||||
it("legt Abteilungen, Personen, Geraete und freie Tags an", async () => {
|
||||
const eigene = await eigeneOrganisation();
|
||||
const bestand = await stammdatenSaeen(testPrisma, eigene.id);
|
||||
|
||||
Reference in New Issue
Block a user