import { describe, expect, it } from 'vitest' import { findTransition, LIFECYCLE_MAIN, LIFECYCLE_SIDE, statusOptions, type CaseStatusName, } from '@/lib/cases/lifecycle' import { canEditCase } from '@/lib/access/caseWrite' import type { EffectivePermissions, Permission } from '@/lib/auth/permissions' /** * Die Zustandskarte. * * Sie ersetzt eine Auswahlliste, die nur die gerade erreichbaren Zustände * enthielt - man sah nie, wo die Akte steht und was es sonst noch gibt, und * nach jedem Wechsel standen andere Einträge darin. * * Zwei Zusicherungen hängen daran, und beide stehen hier: * * 1. Es sind **immer alle** Zustände dabei, in derselben Reihenfolge. Sonst * wandert die Karte wieder. * 2. Was die Karte als schaltbar zeigt, führt der Dienst auch aus. Ein Knopf, * der in eine Absage läuft, ist schlimmer als kein Knopf. */ function perms( clearanceLevel: number, options: { isAdmin?: boolean; permissions?: Permission[] } = {}, ): EffectivePermissions { return { userId: 'u', clearanceLevel, permissions: new Set(options.permissions ?? []), isAdmin: options.isAdmin ?? false, hasMembership: true, } } const ALLE = [...LIFECYCLE_MAIN, ...LIFECYCLE_SIDE] describe('Die Karte zeigt immer dasselbe', () => { it('enthält aus jedem Zustand alle sieben, in gleicher Reihenfolge', () => { const admin = perms(5, { isAdmin: true }) for (const von of ALLE) { const karte = statusOptions(admin, { status: von, classification: 1 }) expect(karte.map((o) => o.status), von).toEqual(ALLE) } }) it('markiert genau einen Zustand als den jetzigen', () => { for (const von of ALLE) { const karte = statusOptions(perms(5, { isAdmin: true }), { status: von, classification: 1 }) const jetzt = karte.filter((o) => o.block?.kind === 'current') expect(jetzt.map((o) => o.status)).toEqual([von]) // Der jetzige Zustand ist kein Ziel. expect(jetzt[0]?.available).toBe(false) } }) }) describe('Was die Karte anbietet, führt der Dienst auch aus', () => { const faelle: { p: EffectivePermissions; name: string }[] = [ { p: perms(5, { isAdmin: true }), name: 'Admin' }, { p: perms(3, { permissions: ['case.edit'] }), name: 'nur case.edit' }, { p: perms(3, { permissions: ['case.edit', 'case.close'] }), name: 'edit und close' }, { p: perms(1, { permissions: ['case.edit'] }), name: 'zu niedrige Stufe' }, { p: perms(3), name: 'ohne jedes Recht' }, ] it('bietet nichts an, was an Übergang oder Recht scheitern würde', () => { // Der Kern: dieselbe Prüfung, die changeStatus anwendet, gegen jede // Zelle der Karte. for (const { p, name } of faelle) { for (const von of ALLE) { const akte = { status: von, classification: 3 } for (const option of statusOptions(p, akte)) { if (!option.available) continue const uebergang = findTransition(von, option.status) expect(uebergang, `${name}: ${von} -> ${option.status}`).toBeDefined() expect( canEditCase(p, akte, uebergang!.permission).ok, `${name}: ${von} -> ${option.status}`, ).toBe(true) } } } }) it('lässt nichts aus, was ginge', () => { // Die Gegenrichtung. Eine Karte, die einen gültigen Weg verschweigt, wäre // genauso unbrauchbar - nur unauffälliger. for (const { p, name } of faelle) { for (const von of ALLE) { const akte = { status: von, classification: 3 } const karte = statusOptions(p, akte) for (const nach of ALLE) { if (nach === von) continue const uebergang = findTransition(von, nach) if (!uebergang) continue if (!canEditCase(p, akte, uebergang.permission).ok) continue expect( karte.find((o) => o.status === nach)?.available, `${name}: ${von} -> ${nach}`, ).toBe(true) } } } }) }) describe('Der Grund für eine Sperre', () => { it('nennt das fehlende Recht', () => { const karte = statusOptions(perms(3), { status: 'OPEN', classification: 1 }) const ruhend = karte.find((o) => o.status === 'DORMANT') expect(ruhend?.available).toBe(false) expect(ruhend?.block).toEqual({ kind: 'no-permission', permission: 'case.edit' }) }) it('unterscheidet "geht von hier nicht" von "darfst du nicht"', () => { // Beides sperrt, aber nur eines lässt sich durch Rechte beheben. Wer das // verwechselt, sucht an der falschen Stelle. const karte = statusOptions(perms(5, { isAdmin: true }), { status: 'DRAFT', classification: 1 }) expect(karte.find((o) => o.status === 'ARCHIVED')?.block).toEqual({ kind: 'unreachable' }) expect(karte.find((o) => o.status === 'OPEN')?.available).toBe(true) }) it('sperrt aus einer archivierten Akte alles, auch für den Admin', () => { const karte = statusOptions(perms(5, { isAdmin: true }), { status: 'ARCHIVED', classification: 1, }) expect(karte.filter((o) => o.available)).toEqual([]) }) }) describe('Was ein Wechsel verlangt', () => { it('meldet die Pflichtbegründung beim Wiedereröffnen', () => { const karte = statusOptions(perms(5, { isAdmin: true }), { status: 'CLOSED', classification: 1, }) expect(karte.find((o) => o.status === 'OPEN')?.requiresReason).toBe(true) }) it('meldet die Pflichtbegründung beim Versiegeln', () => { for (const von of ['DRAFT', 'OPEN', 'REVIEW'] as CaseStatusName[]) { const karte = statusOptions(perms(5, { isAdmin: true }), { status: von, classification: 1 }) expect(karte.find((o) => o.status === 'SEALED')?.requiresReason, von).toBe(true) } }) it('meldet die vollständigen Abschnitte vor dem Abschluss', () => { const karte = statusOptions(perms(5, { isAdmin: true }), { status: 'REVIEW', classification: 1, }) expect(karte.find((o) => o.status === 'CLOSED')?.requiresCompleteSections).toBe(true) }) it('verlangt sonst nichts', () => { const karte = statusOptions(perms(5, { isAdmin: true }), { status: 'DRAFT', classification: 1 }) const offen = karte.find((o) => o.status === 'OPEN') expect(offen?.requiresReason).toBe(false) expect(offen?.requiresCompleteSections).toBe(false) }) })