import { describe, expect, it } from 'vitest' import { createElement } from 'react' import { endungPasst, inhaltBeschreiben, typErkennen, } from '@/lib/speicher/dateityp' import { betreuteIdAusSchluessel, schluesselErzeugen, schluesselGueltig, UngueltigerSchluessel, zugriffTokenErzeugen, zugriffTokenPruefen, } from '@/lib/speicher/schluessel' import { aktenzeichenErkennen, alleAktenzeichen, absenderErkennen, datenFinden, dokumentartErkennen, erkennungsvorschlaege, fristenErkennen, schreibdatumErkennen, } from '@/lib/dokumente/erkennung' import { suchanfrageAufbereiten } from '@/lib/dokumente/suchbegriff' import { bildAlsPdf, paginieren, pdfErzeugen, pdfsZusammenfuehren, seitenzahl, } from '@/lib/pdf/erzeugen' import { Berichtsdokument } from '@/lib/pdf/rahmen' import { standardschriftAufloesen } from '@/lib/pdf/standardschrift' import { ocrLesen } from '@/lib/ocr' import { pdfSeitenzahlSchaetzen, pdfTextLesen, textAufraeumen } from '@/lib/ocr/pdf-text' /** * Diese Tests beschreiben, was fachlich gelten soll - nicht, welche * Funktion aufgerufen wird. Sie brauchen weder Datenbank noch Netz. */ // ----------------------------------------------------------------------------- // Hilfen: kleine, gueltige Dateien im Speicher bauen // ----------------------------------------------------------------------------- function pdfDatei(): Buffer { return Buffer.concat([ Buffer.from('%PDF-1.7\n1 0 obj\n<< /Type /Catalog >>\nendobj\n', 'latin1'), Buffer.from('trailer\n<< /Root 1 0 R >>\n%%EOF\n', 'latin1'), ]) } function jpegDatei(): Buffer { return Buffer.concat([Buffer.from([0xff, 0xd8, 0xff, 0xe0]), Buffer.alloc(64, 0x11)]) } function pngDatei(): Buffer { return Buffer.concat([ Buffer.from([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a]), Buffer.alloc(64, 0x22), ]) } function webpDatei(): Buffer { const kopf = Buffer.alloc(16) kopf.write('RIFF', 0, 'ascii') kopf.writeUInt32LE(64, 4) kopf.write('WEBPVP8 ', 8, 'ascii') return Buffer.concat([kopf, Buffer.alloc(48, 0x33)]) } function tiffDatei(): Buffer { return Buffer.concat([Buffer.from([0x49, 0x49, 0x2a, 0x00]), Buffer.alloc(64, 0x44)]) } function heicDatei(): Buffer { const kopf = Buffer.alloc(24) kopf.writeUInt32BE(24, 0) kopf.write('ftypheic', 4, 'ascii') kopf.write('mif1heic', 12, 'ascii') return Buffer.concat([kopf, Buffer.alloc(48, 0x55)]) } /** Eine HTML-Seite, die sich als PDF ausgibt. */ function getarnteHtmlDatei(): Buffer { return Buffer.from( '\nrechnung.pdf' + '', 'utf8', ) } // ----------------------------------------------------------------------------- // Dateityp // ----------------------------------------------------------------------------- describe('Der Dateityp wird am Inhalt entschieden, nicht am Namen', () => { it('erkennt die sechs erlaubten Formate', () => { expect(typErkennen(pdfDatei())?.mimeTyp).toBe('application/pdf') expect(typErkennen(jpegDatei())?.mimeTyp).toBe('image/jpeg') expect(typErkennen(pngDatei())?.mimeTyp).toBe('image/png') expect(typErkennen(webpDatei())?.mimeTyp).toBe('image/webp') expect(typErkennen(tiffDatei())?.mimeTyp).toBe('image/tiff') expect(typErkennen(heicDatei())?.mimeTyp).toBe('image/heic') }) it('lehnt eine HTML-Seite ab, die sich als PDF ausgibt', () => { const getarnt = getarnteHtmlDatei() expect(typErkennen(getarnt)).toBeNull() expect(inhaltBeschreiben(getarnt)).toBe('eine HTML-Seite') }) it('lehnt ein PDF ab, dem Fremdinhalt vorangestellt wurde', () => { // Der klassische Polyglot: erst HTML, dann irgendwo %PDF. const polyglot = Buffer.concat([Buffer.from('x\n'), pdfDatei()]) expect(typErkennen(polyglot)).toBeNull() }) it('lehnt Office-Dateien, Archive und Programme ab', () => { const zip = Buffer.concat([Buffer.from([0x50, 0x4b, 0x03, 0x04]), Buffer.alloc(32)]) const exe = Buffer.concat([Buffer.from([0x4d, 0x5a]), Buffer.alloc(32)]) expect(typErkennen(zip)).toBeNull() expect(typErkennen(exe)).toBeNull() expect(inhaltBeschreiben(zip)).toContain('ZIP') }) it('lehnt zu kurze und leere Dateien ab', () => { expect(typErkennen(Buffer.alloc(0))).toBeNull() expect(typErkennen(Buffer.from('%PDF'))).toBeNull() expect(inhaltBeschreiben(Buffer.alloc(0))).toBe('eine leere Datei') }) it('merkt, wenn die Endung nicht zum Inhalt passt', () => { const jpeg = typErkennen(jpegDatei())! expect(endungPasst('foto.jpeg', jpeg)).toBe(true) expect(endungPasst('foto.jpg', jpeg)).toBe(true) expect(endungPasst('rechnung.pdf', jpeg)).toBe(false) }) }) // ----------------------------------------------------------------------------- // Ablageschluessel // ----------------------------------------------------------------------------- describe('Der Ablageschlüssel wird erzeugt und lässt sich nicht ausbrechen', () => { it('setzt die betreuteId an die zweite Stelle', () => { const schluessel = schluesselErzeugen('clx123abc', 'dokumente', 'pdf') expect(schluessel).toMatch(/^betreute\/clx123abc\/dokumente\/[0-9a-f-]{36}\.pdf$/) expect(betreuteIdAusSchluessel(schluessel)).toBe('clx123abc') }) it('erzeugt für dieselbe Datei nie zweimal denselben Schlüssel', () => { const a = schluesselErzeugen('clx123abc', 'dokumente', 'pdf') const b = schluesselErzeugen('clx123abc', 'dokumente', 'pdf') expect(a).not.toBe(b) }) it('weist eine Kennung mit Pfadanteilen zurück', () => { expect(() => schluesselErzeugen('../../etc', 'dokumente', 'pdf')).toThrow(UngueltigerSchluessel) expect(() => schluesselErzeugen('a/b', 'dokumente', 'pdf')).toThrow(UngueltigerSchluessel) expect(() => schluesselErzeugen('a.b', 'dokumente', 'pdf')).toThrow(UngueltigerSchluessel) }) it('erkennt von Hand gebastelte Schlüssel als ungültig', () => { expect(schluesselGueltig('betreute/clx1/dokumente/../../../etc/passwd')).toBe(false) expect(schluesselGueltig('betreute/clx1/dokumente/datei.pdf')).toBe(false) expect(schluesselGueltig('betreute/clx1/geheim/00000000-0000-0000-0000-000000000000.pdf')).toBe( false, ) expect(schluesselGueltig('/etc/passwd')).toBe(false) expect(schluesselGueltig('betreute\\clx1\\dokumente\\x.pdf')).toBe(false) expect(schluesselGueltig(null)).toBe(false) }) it('nimmt den Dateinamen des Nutzers nicht in den Pfad auf', () => { const schluessel = schluesselErzeugen('clx123abc', 'dokumente', 'pdf') expect(schluessel).not.toContain('rechnung') expect(schluessel.split('/')).toHaveLength(4) }) }) // ----------------------------------------------------------------------------- // Signierte Links // ----------------------------------------------------------------------------- describe('Signierte Dateilinks laufen ab und lassen sich nicht fälschen', () => { const schluessel = schluesselErzeugen('clx123abc', 'dokumente', 'pdf') const jetzt = new Date('2026-03-12T10:00:00.000Z') it('erkennt ein frisches Token an', () => { const token = zugriffTokenErzeugen(schluessel, 300, jetzt) const geprueft = zugriffTokenPruefen(token, jetzt) expect(geprueft.gueltig).toBe(true) if (geprueft.gueltig) expect(geprueft.schluessel).toBe(schluessel) }) it('weist ein abgelaufenes Token zurück', () => { const token = zugriffTokenErzeugen(schluessel, 300, jetzt) const spaeter = new Date(jetzt.getTime() + 301_000) const geprueft = zugriffTokenPruefen(token, spaeter) expect(geprueft.gueltig).toBe(false) if (!geprueft.gueltig) expect(geprueft.grund).toBe('abgelaufen') }) it('weist ein Token mit verlängertem Ablauf zurück', () => { const token = zugriffTokenErzeugen(schluessel, 60, jetzt) const [kodiert, ablauf, signatur] = token.split('.') const verlaengert = `${kodiert}.${Number(ablauf) + 86_400}.${signatur}` const geprueft = zugriffTokenPruefen(verlaengert, jetzt) expect(geprueft.gueltig).toBe(false) if (!geprueft.gueltig) expect(geprueft.grund).toBe('signatur') }) it('weist ein Token zurück, in das ein fremder Schlüssel geschoben wurde', () => { const token = zugriffTokenErzeugen(schluessel, 300, jetzt) const fremd = schluesselErzeugen('clyFREMD99', 'dokumente', 'pdf') const [, ablauf, signatur] = token.split('.') const gebastelt = `${Buffer.from(fremd, 'utf8').toString('base64url')}.${ablauf}.${signatur}` const geprueft = zugriffTokenPruefen(gebastelt, jetzt) expect(geprueft.gueltig).toBe(false) if (!geprueft.gueltig) expect(geprueft.grund).toBe('signatur') }) it('weist Unsinn und leere Werte zurück', () => { for (const wert of ['', 'abc', 'a.b.c', null, undefined, 12]) { expect(zugriffTokenPruefen(wert as unknown, jetzt).gueltig).toBe(false) } }) }) // ----------------------------------------------------------------------------- // Datum und Aktenzeichen // ----------------------------------------------------------------------------- describe('Datumsangaben im Schreiben werden gelesen', () => { it('versteht 12.03.2026, 12. März 2026 und 2026-03-12', () => { expect(datenFinden('Bayreuth, 12.03.2026')[0]?.datum.toISOString()).toBe( '2026-03-12T00:00:00.000Z', ) expect(datenFinden('Bayreuth, 12. März 2026')[0]?.datum.toISOString()).toBe( '2026-03-12T00:00:00.000Z', ) expect(datenFinden('Stand 2026-03-12')[0]?.datum.toISOString()).toBe( '2026-03-12T00:00:00.000Z', ) }) it('ergänzt zweistellige Jahre sinnvoll', () => { expect(datenFinden('am 12.03.26')[0]?.datum.getUTCFullYear()).toBe(2026) expect(datenFinden('am 12.03.99')[0]?.datum.getUTCFullYear()).toBe(1999) }) it('erfindet keinen 31. Februar', () => { expect(datenFinden('am 31.02.2026')).toHaveLength(0) expect(datenFinden('am 45.13.2026')).toHaveLength(0) }) it('nimmt das ausdrücklich benannte Datum des Schreibens', () => { const text = [ 'Bezirk Oberfranken', 'Sozialverwaltung', '', 'Bayreuth, 05.03.2026', 'Ihr Schreiben vom 12.03.2026', ].join('\n') const vorschlag = schreibdatumErkennen(text, new Date('2026-06-01T00:00:00.000Z')) expect(vorschlag?.wert.toISOString()).toBe('2026-03-12T00:00:00.000Z') expect(vorschlag?.sicherheit).toBe('hoch') }) it('übernimmt kein Datum aus der Zukunft als Briefdatum', () => { const text = 'Zahlbar bis zum 30.12.2030' expect(schreibdatumErkennen(text, new Date('2026-03-12T00:00:00.000Z'))).toBeNull() }) }) describe('Aktenzeichen und Nummern werden erkannt', () => { it('liest das Aktenzeichen des Betreuungsgerichts', () => { const vorschlag = aktenzeichenErkennen('Amtsgericht Bayreuth\nXVII 123/25\n\nBeschluss') expect(vorschlag?.wert).toBe('XVII 123/25') }) it('liest ein beschriftetes Aktenzeichen', () => { expect(aktenzeichenErkennen('Az.: 4711/26\nBetreff: Hilfe zur Pflege')?.wert).toBe('4711/26') expect(aktenzeichenErkennen('Aktenzeichen: SG 12 SO 45/26\n')?.wert).toBe('SG 12 SO 45/26') }) it('liest Kunden- und Versicherungsnummern', () => { expect(aktenzeichenErkennen('Kd.-Nr. 998877\nRechnung')?.wert).toBe('998877') expect(aktenzeichenErkennen('Versicherungsschein-Nr. LV-4711/2\n')?.wert).toBe('LV-4711/2') }) it('hält eine reine Jahreszahl nicht für ein Aktenzeichen', () => { expect(aktenzeichenErkennen('Zeitraum 01/2025 bis 12/2025')).toBeNull() }) it('sammelt mehrere Nummern für die Anzeige', () => { const text = 'Az.: 4711/26\nKundennummer: 998877\n' expect(alleAktenzeichen(text)).toEqual(expect.arrayContaining(['4711/26', '998877'])) }) }) // ----------------------------------------------------------------------------- // Dokumentart und Absender // ----------------------------------------------------------------------------- describe('Die Art des Schreibens wird vorgeschlagen', () => { it('erkennt einen Widerspruchsbescheid als Bescheid', () => { const text = 'Widerspruchsbescheid\n\nIhr Antrag auf Hilfe zur Pflege wird abgelehnt.' expect(dokumentartErkennen(text)?.wert).toBe('BESCHEID') }) it('unterscheidet Mahnung von Rechnung', () => { expect(dokumentartErkennen('Mahnung\nOffener Rechnungsbetrag 120,00 EUR')?.wert).toBe('MAHNUNG') expect(dokumentartErkennen('Rechnung\nRechnungsnummer 4711\nZahlbetrag')?.wert).toBe('RECHNUNG') }) it('erkennt Beschluss, Grundbuchauszug und Kaufvertrag', () => { expect(dokumentartErkennen('Beschluss\nIm Namen des Volkes')?.wert).toBe('BESCHLUSS') expect(dokumentartErkennen('Grundbuchauszug\nGrundbuch von Bayreuth')?.wert).toBe( 'GRUNDBUCHAUSZUG', ) expect(dokumentartErkennen('Kaufvertrag über das Grundstück')?.wert).toBe('VERTRAG') }) it('gibt nichts vor, wenn nichts erkennbar ist', () => { expect(dokumentartErkennen('lorem ipsum dolor')).toBeNull() }) }) describe('Der Absender wird vorgeschlagen', () => { const brief = [ 'Bezirk Oberfranken', 'Sozialverwaltung', '95440 Bayreuth', '', 'Bayreuth, 12.03.2026', '', 'Sehr geehrte Frau Wilde,', ].join('\n') it('bevorzugt einen bereits bekannten Absender', () => { const vorschlag = absenderErkennen(brief, ['Bezirk Oberfranken, Sozialverwaltung']) expect(vorschlag?.wert).toBe('Bezirk Oberfranken, Sozialverwaltung') expect(vorschlag?.sicherheit).toBe('hoch') }) it('erkennt sonst die Behörde im Briefkopf', () => { const vorschlag = absenderErkennen(brief, []) expect(vorschlag?.wert).toBe('Bezirk Oberfranken') expect(vorschlag?.sicherheit).toBe('mittel') }) it('hält die Anrede nicht für den Absender', () => { const vorschlag = absenderErkennen('Sehr geehrte Frau Wilde,\n\nwir teilen Ihnen mit …', []) expect(vorschlag?.wert ?? '').not.toMatch(/Sehr geehrte/) }) }) // ----------------------------------------------------------------------------- // Fristen // ----------------------------------------------------------------------------- describe('Fristen im Text werden vorgeschlagen, nie angelegt', () => { const zugang = new Date(Date.UTC(2026, 2, 12)) // 12.03.2026 it('rechnet "innerhalb von vier Wochen" ab dem Zugangsdatum', () => { const text = 'Gegen diesen Bescheid können Sie innerhalb von vier Wochen nach Bekanntgabe ' + 'Widerspruch einlegen.' const [frist] = fristenErkennen(text, zugang) expect(frist).toBeDefined() expect(frist!.datum?.toISOString()).toBe('2026-04-09T00:00:00.000Z') expect(frist!.basis?.toISOString()).toBe('2026-03-12T00:00:00.000Z') expect(frist!.rechtsbehelf).toBe(true) expect(frist!.textstelle).toContain('vier Wochen') }) it('versteht "binnen eines Monats" und "einen Monat nach Bekanntgabe"', () => { const a = fristenErkennen('Widerspruch binnen eines Monats.', zugang)[0] expect(a?.datum?.toISOString()).toBe('2026-04-12T00:00:00.000Z') const b = fristenErkennen('Der Widerspruch ist einen Monat nach Bekanntgabe zu erheben.', zugang)[0] expect(b?.datum?.toISOString()).toBe('2026-04-12T00:00:00.000Z') }) it('übernimmt ein ausdrücklich genanntes Datum', () => { const [frist] = fristenErkennen('Wir bitten um Zahlung bis zum 30.04.2026.', zugang) expect(frist?.datum?.toISOString()).toBe('2026-04-30T00:00:00.000Z') expect(frist?.sicherheit).toBe('hoch') }) it('hält einen Zeitraum nicht für eine Frist', () => { expect(fristenErkennen('Abrechnung vom 01.01.2025 bis 31.12.2025', zugang)).toHaveLength(0) }) it('liefert ohne Zugangsdatum kein erfundenes Datum', () => { const [frist] = fristenErkennen('Bitte antworten Sie innerhalb von 14 Tagen.', null) expect(frist?.datum).toBeNull() expect(frist?.begruendung).toContain('Eingangsdatum') }) it('nennt bei jedem Vorschlag die Textstelle, auf die er sich stützt', () => { const fristen = fristenErkennen('Zahlung binnen zwei Wochen erbeten.', zugang) expect(fristen[0]?.textstelle.length).toBeGreaterThan(0) expect(fristen[0]?.begruendung).toContain('2 Wochen') }) }) // ----------------------------------------------------------------------------- // Alles zusammen // ----------------------------------------------------------------------------- describe('Ein abfotografierter Bescheid liefert einen brauchbaren Vorschlag', () => { const bescheid = [ 'Bezirk Oberfranken', 'Sozialverwaltung', '95440 Bayreuth', '', 'Bayreuth, 05.03.2026', 'Aktenzeichen: 4711/26', '', 'Betreff: Hilfe zur Pflege nach dem SGB XII', '', 'Widerspruchsbescheid', '', 'Sehr geehrte Frau Wilde,', 'Ihr Widerspruch wird zurückgewiesen. Gegen diesen Bescheid können Sie innerhalb', 'von vier Wochen nach Bekanntgabe Klage erheben.', ].join('\n') it('schlägt Absender, Datum, Aktenzeichen, Art und Frist vor', () => { const ergebnis = erkennungsvorschlaege(bescheid, { bekannteAbsender: ['Bezirk Oberfranken, Sozialverwaltung'], zugangAm: new Date(Date.UTC(2026, 2, 9)), heute: new Date(Date.UTC(2026, 2, 20)), }) expect(ergebnis.absender?.wert).toBe('Bezirk Oberfranken, Sozialverwaltung') expect(ergebnis.schreibenVom?.wert.toISOString()).toBe('2026-03-05T00:00:00.000Z') expect(ergebnis.aktenzeichen?.wert).toBe('4711/26') expect(ergebnis.art?.wert).toBe('BESCHEID') expect(ergebnis.titel).toBe('Hilfe zur Pflege nach dem SGB XII') expect(ergebnis.fristen[0]?.datum?.toISOString()).toBe('2026-04-06T00:00:00.000Z') expect(ergebnis.fristen[0]?.rechtsbehelf).toBe(true) }) it('liefert bei leerem Text keine erfundenen Werte', () => { const leer = erkennungsvorschlaege('', {}) expect(leer.absender).toBeNull() expect(leer.art).toBeNull() expect(leer.fristen).toEqual([]) }) }) // ----------------------------------------------------------------------------- // Suchanfrage // ----------------------------------------------------------------------------- describe('Die Suchanfrage wird für websearch_to_tsquery aufbereitet', () => { it('lässt die gewohnte Suchmaschinen-Schreibweise stehen', () => { expect(suchanfrageAufbereiten('"Hilfe zur Pflege" -Mahnung')).toBe('"Hilfe zur Pflege" -Mahnung') }) it('räumt Leerraum und Steuerzeichen weg', () => { expect(suchanfrageAufbereiten(' Bescheid \n Bezirk \t')).toBe('Bescheid Bezirk') }) it('entfernt ein einzelnes offenes Anführungszeichen', () => { expect(suchanfrageAufbereiten('"Hilfe zur Pflege')).toBe('Hilfe zur Pflege') }) it('gibt nichts zurück, wenn nichts Suchbares übrig bleibt', () => { expect(suchanfrageAufbereiten('')).toBeNull() expect(suchanfrageAufbereiten(' ')).toBeNull() expect(suchanfrageAufbereiten('a')).toBeNull() expect(suchanfrageAufbereiten(null)).toBeNull() }) it('begrenzt die Länge, damit niemand die Datenbank beschäftigt', () => { expect(suchanfrageAufbereiten('x'.repeat(5000))!.length).toBe(200) }) }) // ----------------------------------------------------------------------------- // PDF-Werkzeuge // ----------------------------------------------------------------------------- /** Ein gueltiges 1x1-PNG - genug, um den Bildweg zu pruefen. */ const EIN_PIXEL_PNG = Buffer.from( 'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mP8z8BQDwAEhQGAhKmMIQAAAABJRU5ErkJggg==', 'base64', ) describe('Die eingebauten Schriften werden aufgelöst', () => { it('macht aus Familie plus Auszeichnung den konkreten Schriftnamen', () => { expect(standardschriftAufloesen('Helvetica')).toBe('Helvetica') expect(standardschriftAufloesen('Helvetica', 'bold')).toBe('Helvetica-Bold') expect(standardschriftAufloesen('Helvetica', 700)).toBe('Helvetica-Bold') expect(standardschriftAufloesen('Helvetica', 400, 'italic')).toBe('Helvetica-Oblique') expect(standardschriftAufloesen('Times-Roman', 'bold', 'italic')).toBe('Times-BoldItalic') }) it('lässt einen bereits konkreten Namen unverändert', () => { expect(standardschriftAufloesen('Courier-Bold')).toBe('Courier-Bold') }) it('gibt nichts zurück für Schriften, die nicht eingebaut sind', () => { expect(standardschriftAufloesen('Arial')).toBeNull() }) }) describe('Aus einem Bericht und einem Beleg wird eine Mappe', () => { it('setzt einen Bericht mit Umlauten, ohne eine Schrift nachzuladen', async () => { const bericht = await pdfErzeugen( createElement(Berichtsdokument, { kopf: { titel: 'Vermögensübersicht', betreute: { name: 'Erika Mustermann', geburtsdatum: new Date(Date.UTC(1940, 0, 1)) }, aktenzeichen: 'XVII 123/25', erstelltAm: new Date(Date.UTC(2026, 2, 12)), erstelltVon: 'Florian Wilde', }, mitUnterschrift: true, children: null, }), ) expect(bericht.subarray(0, 5).toString('latin1')).toBe('%PDF-') expect(await seitenzahl(bericht)).toBe(1) }) it('wandelt ein Foto in eine einzelne PDF-Seite', async () => { const alsPdf = await bildAlsPdf(EIN_PIXEL_PNG, 'Beleg 1 · 12.03.2026') expect(await seitenzahl(alsPdf)).toBe(1) }) it('führt Bericht und Foto zu einer Mappe zusammen und nummeriert sie durch', async () => { const deckblatt = await pdfErzeugen( createElement(Berichtsdokument, { kopf: { titel: 'Belegmappe', betreute: { name: 'Erika Mustermann', geburtsdatum: null }, erstelltAm: new Date(Date.UTC(2026, 2, 12)), erstelltVon: 'Florian Wilde', }, children: null, }), ) // Bilder dürfen direkt mitgegeben werden - sie werden unterwegs gewandelt. const mappe = await pdfsZusammenfuehren([deckblatt, EIN_PIXEL_PNG]) expect(await seitenzahl(mappe)).toBe(2) const paginiert = await paginieren(mappe, 'Belegmappe · Mustermann · Az. XVII 123/25') expect(await seitenzahl(paginiert)).toBe(2) expect(paginiert.subarray(0, 5).toString('latin1')).toBe('%PDF-') }) }) // ----------------------------------------------------------------------------- // Texterkennung // ----------------------------------------------------------------------------- describe('Die Texterkennung lässt einen Upload nie scheitern', () => { it('wirft nicht, wenn Tesseract auf diesem Rechner fehlt', async () => { // Auf dem Windows-Entwicklungsrechner ist Tesseract in der Regel nicht // installiert. Dann muss ein Status zurückkommen, keine Ausnahme – das // Dokument gehört auch ohne Volltext in die Akte. const ergebnis = await ocrLesen(pdfDatei(), 'application/pdf') expect(['fertig', 'fehlgeschlagen', 'nicht_verfuegbar']).toContain(ergebnis.status) expect(typeof ergebnis.text).toBe('string') }, 30_000) it('wirft auch bei einer Datei nicht, aus der kein Text zu holen ist', async () => { const ergebnis = await ocrLesen(getarnteHtmlDatei(), 'application/pdf') expect(ergebnis.status).toBe('nicht_verfuegbar') expect(ergebnis.text).toBe('') }, 30_000) }) describe('Eingebetteter PDF-Text wird gelesen, wenn nichts anderes geht', () => { it('liefert eine leere Zeichenkette statt eines Fehlers, wenn nichts drinsteht', () => { expect(pdfTextLesen(pdfDatei())).toBe('') expect(pdfTextLesen(Buffer.alloc(0))).toBe('') }) it('schätzt die Seitenzahl aus der PDF-Struktur', () => { const mitZaehler = Buffer.from( ['%PDF-1.7', '1 0 obj', '/Type /Pages /Count 7 >>', ''].join('\n'), 'latin1', ) expect(pdfSeitenzahlSchaetzen(mitZaehler)).toBe(7) }) it('räumt zerrissenen OCR-Text auf, ohne die Zeilen zu verlieren', () => { // Wagenrücklauf, Mehrfachleerzeichen und Tabulatoren, wie OCR sie liefert. const roh = ['Bezirk Oberfranken', '', '', ' Sozialverwaltung \t'].join('\r\n') expect(textAufraeumen(roh)).toBe(['Bezirk Oberfranken', '', 'Sozialverwaltung'].join('\n')) }) })