docs: README, Datenschutztexte, Dockerfile und Sicherungsskripte
Damit ist Etappe 1 abgeschlossen und betriebsfertig dokumentiert. README mit beiden Betriebswegen. Docker Compose auf eigenem Server ist der Hauptweg; Vercel und Neon sind vollstaendig beschrieben, aber mit einer klaren Begruendung, warum ich davon abrate: beide Anbieter sind US-Unternehmen und unterliegen auch bei EU-Region potenziell dem CLOUD-Act. Bei Gesundheitsdaten einer nicht entscheidungsfaehigen Person ist das eine Abwaegung, die man bewusst treffen muss. Reverse-Proxy-Beispiele fuer Caddy, nginx und Traefik - der Proxy selbst ist nicht Teil des Compose-Setups, weil er auf dem Zielserver schon steht. Die Datenbank hat keine Portfreigabe. Sicherung: pg_dump schreibt direkt in die Verschluesselung, der Klartext landet nie auf der Platte. Danach eine Selbstpruefung - laesst sich die Datei nicht wieder entschluesseln, endet der Lauf mit Fehler statt mit einem stillen Scheinerfolg. Der jeweils erste Stand eines Monats bleibt dauerhaft stehen. Die README sagt ausdruecklich, was das Backup NICHT enthaelt: den Schluessel ENC_KEY_V1 und die Dateien im Objektspeicher. Dazu eine Anleitung, die Wiederherstellung einmal im Jahr zu ueben - mit dem letzten Schritt, in der Anwendung nachzusehen, ob die Diagnosen im Klartext erscheinen. Erst dann stimmt nicht nur die Datenbank, sondern auch der Schluessel. Verarbeitungsverzeichnis und Datenschutzhinweise als ausfuellbare Textbausteine. Die Hinweise sind in einfacher Sprache geschrieben - sie sollen der betreuten Person vorgelesen werden koennen. Die Seite /datenschutz liest denselben Text aus docs/, damit es nicht zwei Fassungen gibt, die auseinanderlaufen. Dockerfile in drei Stufen mit Tesseract, deutschen Sprachdaten und poppler-utils. Zeitzone Europe/Berlin ist gesetzt - ohne sie rechnet der Server in UTC und verschiebt Fristen um einen Tag. Etappe: 1 Phase: Fundament
This commit is contained in:
@@ -0,0 +1,665 @@
|
||||
import { readFileSync } from 'node:fs'
|
||||
import path from 'node:path'
|
||||
import { describe, expect, it } from 'vitest'
|
||||
|
||||
import {
|
||||
ERGAENZEN,
|
||||
GLIEDERUNG,
|
||||
aufzaehlung,
|
||||
dauerAusgeschrieben,
|
||||
entwurfAlsText,
|
||||
jahresberichtEntwurfBauen,
|
||||
jahresberichtEntwurfText,
|
||||
malAngabe,
|
||||
offeneErgaenzungen,
|
||||
textZuAbschnitten,
|
||||
type JahresberichtQuellen,
|
||||
} from '@/lib/berichte/jahresbericht'
|
||||
|
||||
import {
|
||||
berichtDateiname,
|
||||
exportDokumentPfad,
|
||||
pfadIstSicher,
|
||||
sichereEndung,
|
||||
sichererDateiname,
|
||||
belegmappeSchema,
|
||||
unterlagenpaketSchema,
|
||||
jahresberichtErzeugenSchema,
|
||||
} from '@/lib/validierung/berichte'
|
||||
|
||||
import {
|
||||
HinweisdateiSchema,
|
||||
ausloeserPasst,
|
||||
hinweiseAusDaten,
|
||||
hinweiseAusText,
|
||||
hinweiseSuchen,
|
||||
passendeHinweise,
|
||||
type Hinweiskarte,
|
||||
} from '@/lib/hinweise/laden'
|
||||
|
||||
import {
|
||||
beschreibbareModule,
|
||||
freigabeAenderung,
|
||||
freigabenNormalisieren,
|
||||
letzterAdminGeschuetzt,
|
||||
sichtbareModule,
|
||||
} from '@/lib/validierung/freigaben'
|
||||
|
||||
import { darf } from '@/lib/auth/rollen'
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
// Beispieldaten - ein Jahr im Leben einer betreuten Person
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
const tag = (iso: string) => new Date(`${iso}T00:00:00.000Z`)
|
||||
|
||||
function beispiel(aenderungen: Partial<JahresberichtQuellen> = {}): JahresberichtQuellen {
|
||||
const quellen: JahresberichtQuellen = {
|
||||
betreute: {
|
||||
anrede: 'Frau',
|
||||
vorname: 'Maria',
|
||||
nachname: 'Musterfrau',
|
||||
geburtsdatum: tag('1943-04-17'),
|
||||
heimName: 'Seniorenzentrum St. Anna',
|
||||
heimStrasse: 'Klinikweg 4',
|
||||
heimPlz: '95444',
|
||||
heimOrt: 'Bayreuth',
|
||||
heimZimmer: '212',
|
||||
heimEinzugAm: tag('2025-11-03'),
|
||||
fruehereStrasse: 'Bergstraße 12',
|
||||
frueherePlz: '95463',
|
||||
fruehererOrt: 'Bindlach',
|
||||
pflegegrad: 4,
|
||||
pflegegradSeit: tag('2025-12-01'),
|
||||
schwerbehinderungGdB: 80,
|
||||
schwerbehinderungMerkzeichen: 'G, B',
|
||||
diagnosen: 'COPD GOLD III mit Sauerstoffpflicht, Herzinsuffizienz',
|
||||
ermittelterWille: 'Sie möchte nicht wiederbelebt werden und keine künstliche Ernährung.',
|
||||
patientenverfuegungVorhanden: true,
|
||||
vorsorgevollmachtVorhanden: false,
|
||||
aufgabenkreise: ['VERMOEGENSSORGE', 'GESUNDHEITSSORGE'],
|
||||
aufgabenkreiseFreitext: null,
|
||||
betreuungsgericht: 'Amtsgericht Bayreuth',
|
||||
aktenzeichenGericht: 'XVII 123/25',
|
||||
bestellungAm: tag('2025-10-01'),
|
||||
bestellungBis: null,
|
||||
},
|
||||
zeitraumVon: tag('2025-10-01'),
|
||||
zeitraumBis: tag('2026-09-30'),
|
||||
kontakte: [
|
||||
{
|
||||
datum: tag('2025-11-10'),
|
||||
art: 'BESUCH',
|
||||
mitBetreuter: true,
|
||||
dauerMinuten: 90,
|
||||
gespraechspartner: null,
|
||||
wuensche: 'Sie möchte ihre Nähmaschine im Zimmer haben.',
|
||||
},
|
||||
{
|
||||
datum: tag('2026-02-14'),
|
||||
art: 'BESUCH',
|
||||
mitBetreuter: true,
|
||||
dauerMinuten: 60,
|
||||
gespraechspartner: null,
|
||||
wuensche: null,
|
||||
},
|
||||
{
|
||||
datum: tag('2026-06-20'),
|
||||
art: 'TELEFONAT',
|
||||
mitBetreuter: true,
|
||||
dauerMinuten: 15,
|
||||
gespraechspartner: null,
|
||||
wuensche: 'Das Haus in Bindlach soll nicht verkauft werden.',
|
||||
},
|
||||
{
|
||||
datum: tag('2026-03-02'),
|
||||
art: 'TELEFONAT',
|
||||
mitBetreuter: false,
|
||||
dauerMinuten: 20,
|
||||
gespraechspartner: 'Bezirk Oberfranken',
|
||||
wuensche: null,
|
||||
},
|
||||
],
|
||||
gesundheit: [
|
||||
{
|
||||
datum: tag('2026-01-08'),
|
||||
bisDatum: tag('2026-01-19'),
|
||||
art: 'KRANKENHAUS',
|
||||
titel: 'Exazerbation der COPD',
|
||||
einrichtung: 'Klinikum Bayreuth',
|
||||
abwesenheitstage: 11,
|
||||
},
|
||||
{
|
||||
datum: tag('2026-04-03'),
|
||||
bisDatum: null,
|
||||
art: 'ARZTBESUCH',
|
||||
titel: 'Kontrolle Lungenfacharzt',
|
||||
einrichtung: null,
|
||||
abwesenheitstage: null,
|
||||
},
|
||||
],
|
||||
vorgaenge: [
|
||||
{
|
||||
titel: 'Antrag auf Hilfe zur Pflege',
|
||||
kategorie: 'SOZIALHILFE',
|
||||
status: 'WARTET_AUF_DRITTE',
|
||||
naechsterSchritt: 'Sachstandsanfrage beim Bezirk',
|
||||
gegenstelle: 'Bezirk Oberfranken',
|
||||
genehmigungspflichtig: false,
|
||||
erledigtAm: null,
|
||||
},
|
||||
{
|
||||
titel: 'Wohnung Bindlach auflösen',
|
||||
kategorie: 'IMMOBILIE',
|
||||
status: 'ERLEDIGT',
|
||||
naechsterSchritt: null,
|
||||
gegenstelle: null,
|
||||
genehmigungspflichtig: true,
|
||||
erledigtAm: tag('2026-05-12'),
|
||||
},
|
||||
],
|
||||
}
|
||||
return { ...quellen, ...aenderungen }
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
// Jahresbericht
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
describe('Jahresbericht nach § 1863 BGB', () => {
|
||||
it('führt alle fünf Punkte auf, die das Gericht sehen will', () => {
|
||||
const abschnitte = jahresberichtEntwurfBauen(beispiel())
|
||||
|
||||
expect(abschnitte).toHaveLength(5)
|
||||
expect(abschnitte.map((a) => a.nummer)).toEqual([1, 2, 3, 4, 5])
|
||||
expect(abschnitte.map((a) => a.titel)).toEqual(GLIEDERUNG.map((g) => g.titel))
|
||||
for (const abschnitt of abschnitte) {
|
||||
expect(abschnitt.absaetze.length).toBeGreaterThan(0)
|
||||
expect(abschnitt.rechtsgrundlage).toMatch(/§/)
|
||||
}
|
||||
})
|
||||
|
||||
it('beschreibt den Aufenthalt, den Pflegegrad und den Krankenhausaufenthalt', () => {
|
||||
const [persoenlich] = jahresberichtEntwurfBauen(beispiel())
|
||||
const text = persoenlich!.absaetze.join('\n')
|
||||
|
||||
expect(text).toContain('Seniorenzentrum St. Anna')
|
||||
expect(text).toContain('Zimmer 212')
|
||||
expect(text).toContain('Pflegegrad 4')
|
||||
expect(text).toContain('COPD GOLD III')
|
||||
expect(text).toContain('Klinikum Bayreuth')
|
||||
expect(text).toContain('11 Abwesenheitstage')
|
||||
expect(text).toContain('Patientenverfügung liegt vor')
|
||||
})
|
||||
|
||||
it('zählt die persönlichen Kontakte und schreibt sie in ganzen Sätzen aus', () => {
|
||||
const [, kontakt] = jahresberichtEntwurfBauen(beispiel())
|
||||
const text = kontakt!.absaetze.join('\n')
|
||||
|
||||
// Drei Kontakte mit der betreuten Person, davon zwei Besuche.
|
||||
expect(text).toContain('dreimal persönlichen Kontakt')
|
||||
expect(text).toContain('2 Besuche')
|
||||
expect(text).toContain('ein Telefonat')
|
||||
// Der Kontakt mit dem Bezirk zählt nicht als persönlicher Kontakt.
|
||||
expect(text).toContain('1 Kontakte mit Dritten')
|
||||
expect(text).toContain('Bezirk Oberfranken')
|
||||
expect(text).toContain('2 Stunden und 45 Minuten')
|
||||
})
|
||||
|
||||
it('warnt ausdrücklich, wenn kein persönlicher Kontakt protokolliert ist', () => {
|
||||
const ohne = beispiel({ kontakte: [] })
|
||||
const [, kontakt] = jahresberichtEntwurfBauen(ohne)
|
||||
const text = kontakt!.absaetze.join('\n')
|
||||
|
||||
expect(text).toContain('kein persönlicher Kontakt')
|
||||
expect(text).toContain('§ 1863 Abs. 2 BGB')
|
||||
})
|
||||
|
||||
it('übernimmt die Wünsche aus den Protokollen und nennt § 1821 BGB', () => {
|
||||
const [, , wuensche] = jahresberichtEntwurfBauen(beispiel())
|
||||
const text = wuensche!.absaetze.join('\n')
|
||||
|
||||
expect(text).toContain('§ 1821 BGB')
|
||||
expect(text).toContain('Nähmaschine')
|
||||
expect(text).toContain('Das Haus in Bindlach soll nicht verkauft werden.')
|
||||
expect(text).toContain('nicht wiederbelebt werden')
|
||||
// Der Betreuer muss selbst ergänzen, wie er den Wünschen entsprochen hat.
|
||||
expect(text).toContain(ERGAENZEN)
|
||||
})
|
||||
|
||||
it('trennt erledigte und laufende Angelegenheiten nach Kategorien', () => {
|
||||
const [, , , angelegenheiten] = jahresberichtEntwurfBauen(beispiel())
|
||||
const text = angelegenheiten!.absaetze.join('\n')
|
||||
|
||||
expect(text).toContain('Sozialhilfe und Hilfe zur Pflege')
|
||||
expect(text).toContain('Immobilie und Wohnraum')
|
||||
expect(text).toContain('Abgeschlossen wurde Wohnung Bindlach auflösen')
|
||||
expect(text).toContain('Noch offen ist Antrag auf Hilfe zur Pflege')
|
||||
expect(text).toContain('Sachstandsanfrage beim Bezirk')
|
||||
expect(text).toContain('Genehmigungspflichtig waren dabei')
|
||||
})
|
||||
|
||||
it('macht zur Fortführung einen Vorschlag und verlangt eine eigene Entscheidung', () => {
|
||||
const [, , , , einschaetzung] = jahresberichtEntwurfBauen(beispiel())
|
||||
const text = einschaetzung!.absaetze.join('\n')
|
||||
|
||||
expect(text).toContain('Vermögenssorge')
|
||||
expect(text).toContain('Gesundheitssorge')
|
||||
expect(text).toContain('fortzuführen')
|
||||
expect(text).toContain('zu erweitern')
|
||||
expect(text).toContain('einzuschränken')
|
||||
expect(text).toContain(ERGAENZEN)
|
||||
})
|
||||
|
||||
it('bleibt ein Entwurf: offene Stellen sind gezählt und wiederauffindbar', () => {
|
||||
const text = jahresberichtEntwurfText(beispiel())
|
||||
expect(offeneErgaenzungen(text)).toBeGreaterThan(0)
|
||||
})
|
||||
|
||||
it('lässt sich als Text ablegen und wieder in Abschnitte zerlegen', () => {
|
||||
const abschnitte = jahresberichtEntwurfBauen(beispiel())
|
||||
const text = entwurfAlsText(abschnitte)
|
||||
|
||||
expect(text.startsWith('1. Persönliche Verhältnisse')).toBe(true)
|
||||
|
||||
const zurueck = textZuAbschnitten(text)
|
||||
expect(zurueck).toHaveLength(5)
|
||||
expect(zurueck[1]!.titel).toBe('2. Persönlicher Kontakt zur betreuten Person')
|
||||
expect(zurueck[4]!.absaetze.length).toBeGreaterThan(0)
|
||||
})
|
||||
|
||||
it('verkraftet einen leeren Bestand, ohne den Bericht zu verstümmeln', () => {
|
||||
const leer = beispiel({ kontakte: [], gesundheit: [], vorgaenge: [] })
|
||||
const abschnitte = jahresberichtEntwurfBauen(leer)
|
||||
|
||||
expect(abschnitte).toHaveLength(5)
|
||||
for (const abschnitt of abschnitte) {
|
||||
expect(abschnitt.absaetze.join('').trim()).not.toBe('')
|
||||
}
|
||||
})
|
||||
|
||||
it('formuliert Zahl- und Zeitangaben so, wie man sie schreiben würde', () => {
|
||||
expect(malAngabe(0)).toBe('keinmal')
|
||||
expect(malAngabe(1)).toBe('einmal')
|
||||
expect(malAngabe(3)).toBe('dreimal')
|
||||
expect(malAngabe(17)).toBe('17-mal')
|
||||
expect(dauerAusgeschrieben(95)).toBe('1 Stunde und 35 Minuten')
|
||||
expect(dauerAusgeschrieben(45)).toBe('45 Minuten')
|
||||
expect(aufzaehlung(['a', 'b', 'c'])).toBe('a, b und c')
|
||||
expect(aufzaehlung(['a'])).toBe('a')
|
||||
expect(aufzaehlung([])).toBe('')
|
||||
})
|
||||
})
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
// Dateinamen im Export
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
describe('Dateinamen im Vollexport', () => {
|
||||
it('schreibt Umlaute um und lässt nur harmlose Zeichen stehen', () => {
|
||||
expect(sichererDateiname('Bescheid Hilfe zur Pflege')).toBe('Bescheid-Hilfe-zur-Pflege')
|
||||
expect(sichererDateiname('Vergütung & Rückkauf')).toBe('Verguetung-Rueckkauf')
|
||||
expect(sichererDateiname('Maßnahme')).toBe('Massnahme')
|
||||
expect(sichererDateiname(' ')).toBe('ohne-titel')
|
||||
expect(sichererDateiname('...')).toBe('ohne-titel')
|
||||
})
|
||||
|
||||
it('lässt keinen Pfadausbruch zu', () => {
|
||||
for (const boesartig of [
|
||||
'../../etc/passwd',
|
||||
'..\\..\\windows\\system32',
|
||||
'C:\\Users\\Public\\x',
|
||||
'/etc/shadow',
|
||||
'beleg\u0000.pdf',
|
||||
'auszug:stream',
|
||||
]) {
|
||||
const sauber = sichererDateiname(boesartig)
|
||||
expect(sauber).not.toContain('..')
|
||||
expect(sauber).not.toContain('/')
|
||||
expect(sauber).not.toContain('\\')
|
||||
expect(sauber).not.toContain(':')
|
||||
expect(sauber).not.toContain('\u0000')
|
||||
}
|
||||
})
|
||||
|
||||
it('umgeht Namen, die Windows nie akzeptiert', () => {
|
||||
expect(sichererDateiname('CON')).toBe('_CON')
|
||||
expect(sichererDateiname('lpt1')).toBe('_lpt1')
|
||||
})
|
||||
|
||||
it('baut den Dokumentpfad nach dem verabredeten Muster', () => {
|
||||
const pfad = exportDokumentPfad({
|
||||
datum: tag('2026-03-14'),
|
||||
absender: 'Bezirk Oberfranken',
|
||||
titel: 'Bescheid über Hilfe zur Pflege',
|
||||
dateiname: 'scan.PDF',
|
||||
mimeTyp: 'application/pdf',
|
||||
})
|
||||
|
||||
expect(pfad).toBe(
|
||||
'dokumente/2026/2026-03-14_Bezirk-Oberfranken_Bescheid-ueber-Hilfe-zur-Pflege.pdf',
|
||||
)
|
||||
expect(pfadIstSicher(pfad)).toBe(true)
|
||||
})
|
||||
|
||||
it('legt Dokumente ohne Datum in einen eigenen Ordner statt in ein erfundenes Jahr', () => {
|
||||
const pfad = exportDokumentPfad({
|
||||
datum: null,
|
||||
absender: null,
|
||||
titel: '../../geheim',
|
||||
dateiname: null,
|
||||
mimeTyp: 'image/jpeg',
|
||||
})
|
||||
|
||||
expect(pfad.startsWith('dokumente/ohne-jahr/')).toBe(true)
|
||||
expect(pfad.endsWith('.jpg')).toBe(true)
|
||||
expect(pfad.split('/')).toHaveLength(3)
|
||||
expect(pfadIstSicher(pfad)).toBe(true)
|
||||
})
|
||||
|
||||
it('erkennt unzulässige Archivpfade', () => {
|
||||
expect(pfadIstSicher('daten/frist.json')).toBe(true)
|
||||
expect(pfadIstSicher('dokumente/../../x')).toBe(false)
|
||||
expect(pfadIstSicher('/absolut')).toBe(false)
|
||||
expect(pfadIstSicher('C:/windows')).toBe(false)
|
||||
expect(pfadIstSicher('a\\b')).toBe(false)
|
||||
expect(pfadIstSicher('')).toBe(false)
|
||||
expect(pfadIstSicher('a//b')).toBe(false)
|
||||
})
|
||||
|
||||
it('normalisiert Dateiendungen', () => {
|
||||
expect(sichereEndung('scan.PDF')).toBe('pdf')
|
||||
expect(sichereEndung('pdf')).toBe('pdf')
|
||||
expect(sichereEndung('ohnepunkt')).toBe('bin')
|
||||
expect(sichereEndung('bild.jpeg')).toBe('jpeg')
|
||||
expect(sichereEndung('x.verylongextension')).toBe('bin')
|
||||
expect(sichereEndung(null)).toBe('bin')
|
||||
})
|
||||
|
||||
it('benennt erzeugte Berichte sortierbar', () => {
|
||||
expect(berichtDateiname('jahresbericht', 'Musterfrau', tag('2026-09-30'), 'pdf')).toBe(
|
||||
'2026-09-30_jahresbericht_Musterfrau.pdf',
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
// Eingabepruefung
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
describe('Eingabeprüfung der Berichtsformulare', () => {
|
||||
it('weist einen Berichtszeitraum zurück, der rückwärts läuft', () => {
|
||||
const ergebnis = jahresberichtErzeugenSchema.safeParse({
|
||||
betreuteId: 'abc123',
|
||||
zeitraumVon: '2026-09-30',
|
||||
zeitraumBis: '2025-10-01',
|
||||
})
|
||||
expect(ergebnis.success).toBe(false)
|
||||
})
|
||||
|
||||
it('verlangt für die Belegmappe entweder einen Zeitraum oder einen Vorgang', () => {
|
||||
expect(
|
||||
belegmappeSchema.safeParse({ betreuteId: 'abc', quelle: 'zeitraum', zeitraumVon: '', zeitraumBis: '' })
|
||||
.success,
|
||||
).toBe(false)
|
||||
expect(
|
||||
belegmappeSchema.safeParse({
|
||||
betreuteId: 'abc',
|
||||
quelle: 'vorgang',
|
||||
zeitraumVon: '',
|
||||
zeitraumBis: '',
|
||||
vorgangId: 'vg1',
|
||||
}).success,
|
||||
).toBe(true)
|
||||
})
|
||||
|
||||
it('lässt ein Unterlagenpaket ohne ausgewählte Unterlage nicht zu', () => {
|
||||
expect(
|
||||
unterlagenpaketSchema.safeParse({ betreuteId: 'a', antragId: 'b', nachforderungIds: [] })
|
||||
.success,
|
||||
).toBe(false)
|
||||
})
|
||||
})
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
// Hinweis-Wissensbasis
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
const WISSENSBASIS = path.join(process.cwd(), 'daten', 'hinweise.json')
|
||||
|
||||
describe('Hinweis-Wissensbasis', () => {
|
||||
const roh = readFileSync(WISSENSBASIS, 'utf8')
|
||||
const geladen = hinweiseAusText(roh)
|
||||
|
||||
it('ist gültig gegen das Schema', () => {
|
||||
expect(geladen.fehler).toEqual([])
|
||||
expect(HinweisdateiSchema.safeParse(JSON.parse(roh)).success).toBe(true)
|
||||
})
|
||||
|
||||
it('enthält mindestens 25 Karten mit eindeutigen Schlüsseln', () => {
|
||||
expect(geladen.karten.length).toBeGreaterThanOrEqual(25)
|
||||
const schluessel = geladen.karten.map((k) => k.schluessel)
|
||||
expect(new Set(schluessel).size).toBe(schluessel.length)
|
||||
})
|
||||
|
||||
it('nennt zu jeder Karte eine Rechtsgrundlage und einen sicheren Link', () => {
|
||||
for (const karte of geladen.karten) {
|
||||
expect(karte.rechtsgrundlage.length).toBeGreaterThan(1)
|
||||
expect(karte.link.startsWith('https://')).toBe(true)
|
||||
expect(karte.kontext.length).toBeGreaterThan(0)
|
||||
}
|
||||
})
|
||||
|
||||
it('deckt die fachlich unverzichtbaren Themen ab', () => {
|
||||
const schluessel = new Set(geladen.karten.map((k) => k.schluessel))
|
||||
for (const pflicht of [
|
||||
'wohnraum-aufgeben',
|
||||
'grundstueck-genehmigung',
|
||||
'genehmigung-vor-geschaeft',
|
||||
'aufwandspauschale',
|
||||
'vermoegensverzeichnis-frist',
|
||||
'jahresbericht',
|
||||
'getrennte-konten',
|
||||
'schonvermoegen',
|
||||
'hausgrundstueck-geschuetzt',
|
||||
'bestattungsvorsorge',
|
||||
'darlehen-statt-verwertung',
|
||||
'widerspruchsfrist',
|
||||
'fehlende-rechtsbehelfsbelehrung',
|
||||
'wille-leitschnur',
|
||||
'zwangsmassnahme',
|
||||
'unterbringung',
|
||||
'leistungszuschlag',
|
||||
'zuzahlungsbefreiung',
|
||||
'landespflegegeld',
|
||||
'rundfunkbeitragsbefreiung',
|
||||
'pflegegrad-hoeherstufung',
|
||||
'leerstand-versicherung',
|
||||
'verkehrssicherungspflicht',
|
||||
'schenkungsrueckforderung',
|
||||
'betreuerausweis',
|
||||
'kontoauszuege-aufbewahren',
|
||||
'rechtspfleger-fragen',
|
||||
]) {
|
||||
expect(schluessel.has(pflicht), `Karte "${pflicht}" fehlt`).toBe(true)
|
||||
}
|
||||
})
|
||||
|
||||
it('markiert die Ausschlussfrist der Aufwandspauschale als solche', () => {
|
||||
const karte = geladen.karten.find((k) => k.schluessel === 'aufwandspauschale')!
|
||||
expect(karte.text).toContain('Ausschlussfrist')
|
||||
expect(karte.rechtsgrundlage).toContain('1878')
|
||||
})
|
||||
|
||||
it('meldet kaputtes JSON, statt abzustürzen', () => {
|
||||
const ergebnis = hinweiseAusText('{ das ist kein JSON')
|
||||
expect(ergebnis.karten).toEqual([])
|
||||
expect(ergebnis.fehler.length).toBeGreaterThan(0)
|
||||
})
|
||||
|
||||
it('lehnt Karten ohne Pflichtangaben ab', () => {
|
||||
const ergebnis = hinweiseAusDaten({
|
||||
version: 1,
|
||||
karten: [{ schluessel: 'x', titel: 'zu kurz' }],
|
||||
})
|
||||
expect(ergebnis.karten).toEqual([])
|
||||
expect(ergebnis.fehler.length).toBeGreaterThan(0)
|
||||
})
|
||||
|
||||
it('erkennt doppelte Schlüssel', () => {
|
||||
const eine = geladen.karten[0]!
|
||||
const ergebnis = hinweiseAusDaten({ version: 1, karten: [eine, { ...eine }] })
|
||||
expect(ergebnis.karten).toEqual([])
|
||||
expect(ergebnis.fehler[0]).toContain('Doppelte Schlüssel')
|
||||
})
|
||||
})
|
||||
|
||||
describe('Zuordnung der Hinweiskarten zum Kontext', () => {
|
||||
const karten: Hinweiskarte[] = [
|
||||
{
|
||||
schluessel: 'a',
|
||||
titel: 'Karte A für die Immobilie',
|
||||
text: 'x'.repeat(60),
|
||||
rechtsgrundlage: '§ 1833 BGB',
|
||||
link: 'https://www.gesetze-im-internet.de/bgb/__1833.html',
|
||||
kontext: ['IMMOBILIE', 'VORLAGE:wohnungsaufloesung'],
|
||||
gewicht: 5,
|
||||
},
|
||||
{
|
||||
schluessel: 'b',
|
||||
titel: 'Karte B zur Jahresberichtsfrist',
|
||||
text: 'y'.repeat(60),
|
||||
rechtsgrundlage: '§ 1863 BGB',
|
||||
link: 'https://www.gesetze-im-internet.de/bgb/__1863.html',
|
||||
kontext: ['FRIST:jahresbericht'],
|
||||
gewicht: 9,
|
||||
},
|
||||
{
|
||||
schluessel: 'c',
|
||||
titel: 'Karte C zur Sozialhilfe',
|
||||
text: 'z'.repeat(60),
|
||||
rechtsgrundlage: '§ 90 SGB XII',
|
||||
link: 'https://www.gesetze-im-internet.de/sgb_12/__90.html',
|
||||
kontext: ['SOZIALHILFE', 'IMMOBILIE'],
|
||||
gewicht: 9,
|
||||
},
|
||||
]
|
||||
|
||||
it('findet Karten über Modul, Vorlage und Frist', () => {
|
||||
expect(passendeHinweise(karten, ['IMMOBILIE']).map((k) => k.schluessel)).toEqual(['c', 'a'])
|
||||
expect(passendeHinweise(karten, ['VORLAGE:wohnungsaufloesung']).map((k) => k.schluessel)).toEqual([
|
||||
'a',
|
||||
])
|
||||
expect(passendeHinweise(karten, ['FRISTEN'])).toEqual([])
|
||||
})
|
||||
|
||||
it('lässt einen Auslöser auch auf die Frist mit Jahreszahl passen', () => {
|
||||
expect(ausloeserPasst('FRIST:jahresbericht', 'FRIST:jahresbericht-2027')).toBe(true)
|
||||
expect(ausloeserPasst('FRIST:jahresbericht', 'FRIST:jahresberichtXYZ')).toBe(false)
|
||||
expect(ausloeserPasst('IMMOBILIE', 'immobilie')).toBe(true)
|
||||
expect(passendeHinweise(karten, ['FRIST:jahresbericht-2027']).map((k) => k.schluessel)).toEqual([
|
||||
'b',
|
||||
])
|
||||
})
|
||||
|
||||
it('sortiert nach Gewicht und bleibt bei Gleichstand stabil', () => {
|
||||
const treffer = passendeHinweise(karten, ['IMMOBILIE', 'FRIST:jahresbericht'])
|
||||
expect(treffer.map((k) => k.schluessel)).toEqual(['b', 'c', 'a'])
|
||||
expect(passendeHinweise(karten, ['IMMOBILIE'], 1).map((k) => k.schluessel)).toEqual(['c'])
|
||||
})
|
||||
|
||||
it('sucht über Titel, Text und Rechtsgrundlage', () => {
|
||||
expect(hinweiseSuchen(karten, '1833').map((k) => k.schluessel)).toEqual(['a'])
|
||||
expect(hinweiseSuchen(karten, 'sozialhilfe').map((k) => k.schluessel)).toEqual(['c'])
|
||||
expect(hinweiseSuchen(karten, '').map((k) => k.schluessel)).toEqual(['b', 'c', 'a'])
|
||||
})
|
||||
})
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
// Rollen und Freigaben
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
describe('Mitleser-Rolle und Modulfreigaben', () => {
|
||||
const mitleser = {
|
||||
rolle: 'MITLESER' as const,
|
||||
freigegebeneModule: ['STAMMDATEN', 'FRISTEN'] as const,
|
||||
}
|
||||
|
||||
it('lässt einen Mitleser niemals schreiben – auch nicht im freigegebenen Modul', () => {
|
||||
expect(darf({ ...mitleser, freigegebeneModule: [...mitleser.freigegebeneModule] }, 'FRISTEN', 'lesen')).toBe(true)
|
||||
expect(darf({ ...mitleser, freigegebeneModule: [...mitleser.freigegebeneModule] }, 'FRISTEN', 'schreiben')).toBe(false)
|
||||
expect(
|
||||
beschreibbareModule({ rolle: 'MITLESER', freigegebeneModule: ['STAMMDATEN', 'FRISTEN'] }),
|
||||
).toEqual([])
|
||||
})
|
||||
|
||||
it('zeigt einem Mitleser nur die freigegebenen Bereiche', () => {
|
||||
expect(sichtbareModule({ rolle: 'MITLESER', freigegebeneModule: ['STAMMDATEN', 'FRISTEN'] })).toEqual([
|
||||
'STAMMDATEN',
|
||||
'FRISTEN',
|
||||
])
|
||||
expect(darf({ rolle: 'MITLESER', freigegebeneModule: ['STAMMDATEN'] }, 'GESUNDHEIT', 'lesen')).toBe(
|
||||
false,
|
||||
)
|
||||
})
|
||||
|
||||
it('gibt dem Betreuer und dem Administrator alles', () => {
|
||||
expect(sichtbareModule({ rolle: 'BETREUER', freigegebeneModule: [] }).length).toBeGreaterThan(10)
|
||||
expect(beschreibbareModule({ rolle: 'ADMIN', freigegebeneModule: [] }).length).toBeGreaterThan(10)
|
||||
})
|
||||
|
||||
it('sperrt jeden Zugriff ohne Zugang', () => {
|
||||
expect(darf(null, 'STAMMDATEN', 'lesen')).toBe(false)
|
||||
expect(sichtbareModule(null)).toEqual([])
|
||||
})
|
||||
|
||||
it('räumt die Modulliste auf, sobald jemand kein Mitleser mehr ist', () => {
|
||||
expect(freigabenNormalisieren('BETREUER', ['GESUNDHEIT'])).toEqual([])
|
||||
expect(freigabenNormalisieren('ADMIN', ['GESUNDHEIT'])).toEqual([])
|
||||
expect(freigabenNormalisieren('MITLESER', ['FRISTEN', 'STAMMDATEN'])).toEqual([
|
||||
'STAMMDATEN',
|
||||
'FRISTEN',
|
||||
])
|
||||
})
|
||||
|
||||
it('benennt, was sich an einer Freigabe ändert – und was davon heikel ist', () => {
|
||||
const aenderung = freigabeAenderung(
|
||||
{ rolle: 'MITLESER', freigegebeneModule: ['STAMMDATEN', 'FRISTEN'] },
|
||||
{ rolle: 'MITLESER', freigegebeneModule: ['STAMMDATEN', 'GESUNDHEIT', 'FINANZEN'] },
|
||||
)
|
||||
|
||||
expect(aenderung.hinzugefuegt).toEqual(['FINANZEN', 'GESUNDHEIT'])
|
||||
expect(aenderung.entfernt).toEqual(['FRISTEN'])
|
||||
expect(aenderung.sensibelHinzugefuegt).toEqual(['FINANZEN', 'GESUNDHEIT'])
|
||||
expect(aenderung.rolleGeaendert).toBe(false)
|
||||
})
|
||||
|
||||
it('schützt den letzten Administrator vor sich selbst', () => {
|
||||
expect(
|
||||
letzterAdminGeschuetzt({
|
||||
zielRolleVorher: 'ADMIN',
|
||||
zielRolleNachher: 'BETREUER',
|
||||
zielBleibtAktiv: true,
|
||||
anzahlAktiverAdmins: 1,
|
||||
}),
|
||||
).toMatch(/letzte aktive Administrator/)
|
||||
|
||||
expect(
|
||||
letzterAdminGeschuetzt({
|
||||
zielRolleVorher: 'ADMIN',
|
||||
zielRolleNachher: 'BETREUER',
|
||||
zielBleibtAktiv: true,
|
||||
anzahlAktiverAdmins: 2,
|
||||
}),
|
||||
).toBeNull()
|
||||
|
||||
expect(
|
||||
letzterAdminGeschuetzt({
|
||||
zielRolleVorher: 'BETREUER',
|
||||
zielRolleNachher: 'MITLESER',
|
||||
zielBleibtAktiv: true,
|
||||
anzahlAktiverAdmins: 1,
|
||||
}),
|
||||
).toBeNull()
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user