import * as fs from 'fs'; import { afterEach, describe, expect, it, vi } from 'vitest'; import { DkvService } from './dkv.service'; /** * DkvService.spec — RED-first (TDD) Nachweis fuer die Bindung an * forTenant() (260909-mir, Aufgabe 2/3). Dieser Bereich hatte VOR diesem * Durchlauf KEINE einzige Testdatei (Befund J) — dieser Fake ist deshalb die * Voraussetzung dafuer, dass irgendeine Aussage dieses Plans nachpruefbar * ist, nicht eine Zugabe. * * Zwei-Klienten-Nachweis (Muster aus groups.service.spec.ts / * tender-triage.service.spec.ts): `__makeBoundClient(tenantId)` wrappt * DIESELBEN In-Memory-Maps mit einer protokollierenden Schicht je Modell. * Der ungebundene Fake protokolliert NICHT, der gebundene schon — eine * vergessene Bindung wird dadurch sichtbar, ein reiner Identitaets-Mock * (`(p) => p`, der ldap-Fehler) wuerde das nicht leisten. * * Der Fake wird in Aufgabe 3 um `dkvVehicleMaster`/`dkvInvoiceHistory` * ERWEITERT, nicht ersetzt (siehe unten in makeFakePrisma()). */ vi.mock('../prisma/prisma-tenant.extension', () => ({ forTenant: vi.fn((prisma: any, tenantId: string) => prisma.__makeBoundClient(tenantId)), })); // `import * as fs from 'fs'` under ESM has a non-configurable module // namespace — vi.spyOn(fs, 'existsSync') fails with "Cannot redefine // property". vi.mock() replaces the module at import time instead, which // works regardless of namespace configurability (Tests 8-10, Aufgabe 3). vi.mock('fs', async (importOriginal) => { const actual = await importOriginal(); return { ...actual, existsSync: vi.fn(), readFileSync: vi.fn() }; }); function _applySelect(row: any, select: Record | undefined) { if (!select) return { ...row }; const out: Record = {}; for (const key of Object.keys(select)) { if (select[key]) out[key] = (row as any)[key]; } return out; } function makeFakePrisma() { const configs = new Map(); // key: tenantId const boundCallLog: { tenantId: string; model: string; method: string }[] = []; const dkvModuleConfig = { findFirst: vi.fn(async ({ select }: { select?: Record } = {}) => { // Arbitrary-but-first row — die Form, die der Planer-Startpfad heute // benutzt (findFirst() ganz ohne Bedingung). Map bewahrt // Einfuegereihenfolge, das genuegt fuer "irgendeine" Zeile. const first = configs.values().next().value; return first ? _applySelect(first, select) : null; }), findUnique: vi.fn( async ({ where, select, }: { where: { tenantId: string }; select?: Record; }) => { const row = configs.get(where.tenantId); return row ? _applySelect(row, select) : null; }, ), upsert: vi.fn( async ({ where, create, update, select, }: { where: { tenantId: string }; create: Record; update: Record; select?: Record; }) => { const existing = configs.get(where.tenantId); const record = existing ? { ...existing, ...update } : { id: `cfg-${configs.size + 1}`, ...create }; configs.set(where.tenantId, record); return _applySelect(record, select); }, ), }; // ─── dkvVehicleMaster (Aufgabe 3) ───────────────────────────────────────── const vehicles = new Map(); // key: id let vehicleAutoId = 0; const dkvVehicleMaster = { findMany: vi.fn(async ({ where }: { where?: { tenantId?: string } } = {}) => { return Array.from(vehicles.values()).filter( (v) => !where?.tenantId || v.tenantId === where.tenantId, ); }), create: vi.fn(async ({ data }: { data: Record }) => { const id = `veh-${++vehicleAutoId}`; const record = { id, ...data }; vehicles.set(id, record); return record; }), findFirst: vi.fn(async ({ where }: { where: { id: string; tenantId: string } }) => { const row = vehicles.get(where.id); return row && row.tenantId === where.tenantId ? row : null; }), update: vi.fn(async ({ where, data }: { where: { id: string }; data: Record }) => { const existing = vehicles.get(where.id); const record = { ...existing, ...data }; vehicles.set(where.id, record); return record; }), delete: vi.fn(async ({ where }: { where: { id: string } }) => { const existing = vehicles.get(where.id); vehicles.delete(where.id); return existing; }), deleteMany: vi.fn(async ({ where }: { where?: { tenantId?: string } } = {}) => { let count = 0; for (const [id, v] of vehicles) { if (!where?.tenantId || v.tenantId === where.tenantId) { vehicles.delete(id); count++; } } return { count }; }), createMany: vi.fn(async ({ data }: { data: Record[] }) => { for (const d of data) { const id = `veh-${++vehicleAutoId}`; vehicles.set(id, { id, ...d }); } return { count: data.length }; }), upsert: vi.fn( async ({ where, create, update, }: { where: { tenantId_kennzeichen: { tenantId: string; kennzeichen: string } }; create: Record; update: Record; }) => { const key = where.tenantId_kennzeichen; const existing = Array.from(vehicles.values()).find( (v) => v.tenantId === key.tenantId && v.kennzeichen === key.kennzeichen, ); if (existing) { const record = { ...existing, ...update }; vehicles.set(existing.id, record); return record; } const id = `veh-${++vehicleAutoId}`; const record = { id, ...create }; vehicles.set(id, record); return record; }, ), }; // ─── dkvInvoiceHistory (Aufgabe 3) ──────────────────────────────────────── const history = new Map(); // key: id let historyAutoId = 0; const dkvInvoiceHistory = { findMany: vi.fn( async ({ where, skip, take, }: { where?: { tenantId?: string }; skip?: number; take?: number } = {}) => { let rows = Array.from(history.values()).filter( (h) => !where?.tenantId || h.tenantId === where.tenantId, ); if (typeof skip === 'number') rows = rows.slice(skip); if (typeof take === 'number') rows = rows.slice(0, take); return rows; }, ), count: vi.fn(async ({ where }: { where?: { tenantId?: string } } = {}) => { return Array.from(history.values()).filter( (h) => !where?.tenantId || h.tenantId === where.tenantId, ).length; }), create: vi.fn(async ({ data }: { data: Record }) => { const id = `hist-${++historyAutoId}`; const record = { id, ...data }; history.set(id, record); return record; }), findFirst: vi.fn( async ({ where, }: { where: { tenantId?: string; exportFilename?: string }; }) => { return ( Array.from(history.values()).find( (h) => (!where.tenantId || h.tenantId === where.tenantId) && (!where.exportFilename || h.exportFilename === where.exportFilename), ) ?? null ); }, ), }; const fake: any = { dkvModuleConfig, dkvVehicleMaster, dkvInvoiceHistory, __boundCallLog: boundCallLog, __seedConfig(tenantId: string, row: Record) { configs.set(tenantId, { tenantId, ...row }); }, __seedVehicle(row: { id: string; tenantId: string; kennzeichen: string; marke?: string; modell?: string; fahrer?: string }) { vehicles.set(row.id, { marke: '', modell: '', fahrer: '', ...row }); }, __seedHistory(row: { id: string; tenantId: string; exportFilename?: string | null }) { history.set(row.id, { rechnungsnummer: 'RG-TEST', anzahlFahrzeuge: 0, anzahlTransaktionen: 0, status: 'Verarbeitet', exportFilename: null, ...row, }); }, __makeBoundClient(tenantId: string) { const wrapModel = (model: Record, modelName: string, methods: string[]) => { const wrapped: any = {}; for (const method of methods) { wrapped[method] = async (...args: any[]) => { boundCallLog.push({ tenantId, model: modelName, method }); return model[method](...args); }; } return wrapped; }; return { dkvModuleConfig: wrapModel(dkvModuleConfig, 'dkvModuleConfig', ['findFirst', 'findUnique', 'upsert']), dkvVehicleMaster: wrapModel(dkvVehicleMaster, 'dkvVehicleMaster', [ 'findMany', 'create', 'findFirst', 'update', 'delete', 'deleteMany', 'createMany', 'upsert', ]), dkvInvoiceHistory: wrapModel(dkvInvoiceHistory, 'dkvInvoiceHistory', [ 'findMany', 'count', 'create', 'findFirst', ]), }; }, }; return fake; } function expectBoundCall(prisma: any, tenantId: string, model: string, method: string) { const found = prisma.__boundCallLog.some( (c: any) => c.tenantId === tenantId && c.model === model && c.method === method, ); expect( found, `erwarteter gebundener Aufruf ${model}.${method}(tenant=${tenantId}) fehlt im Protokoll: ${JSON.stringify(prisma.__boundCallLog)}`, ).toBe(true); } /** Schlichte Attrappen fuer die uebrigen Konstruktor-Abhaengigkeiten von DkvService. */ function makeFakeCrypto(overrides: Partial<{ encrypt: any; decrypt: any }> = {}) { return { encrypt: overrides.encrypt ?? vi.fn((plain: string) => `enc(${plain})`), decrypt: overrides.decrypt ?? vi.fn((stored: string) => stored.replace(/^enc\(/, '').replace(/\)$/, '')), }; } function makeDkvService( prisma: any, overrides: Partial<{ encrypt: any; decrypt: any; parser: any; exporter: any; mailer: any; imapProvider: any; exchangeProvider: any; }> = {}, ) { const crypto = makeFakeCrypto(overrides); const parser = overrides.parser ?? ({} as any); const exporter = overrides.exporter ?? ({} as any); const mailer = overrides.mailer ?? ({ sendExportEmail: vi.fn() } as any); const imapProvider = overrides.imapProvider ?? ({ testConnection: vi.fn(async () => ({ success: true })), fetchPdfAttachments: vi.fn(async () => []) } as any); const exchangeProvider = overrides.exchangeProvider ?? ({ testConnection: vi.fn(async () => ({ success: true })) } as any); const service = new DkvService( prisma, crypto as any, parser, exporter, mailer, imapProvider, exchangeProvider, ); return { service, crypto }; } describe('DkvService — Bindung an forTenant() (260909-mir)', () => { it('Test 1: getConfigForApi(tenantId) — beide Lesezugriffe auf dkvModuleConfig stehen im Bindungsprotokoll unter genau diesem Mandanten', async () => { const prisma = makeFakePrisma(); prisma.__seedConfig('t1', { id: 'cfg-1', protocol: 'imap', isActive: true, encryptedInboxCreds: null }); const { service } = makeDkvService(prisma); await service.getConfigForApi('t1'); const configCalls = prisma.__boundCallLog.filter( (c: any) => c.tenantId === 't1' && c.model === 'dkvModuleConfig' && c.method === 'findUnique', ); expect( configCalls.length, `erwarte mindestens zwei gebundene findUnique-Aufrufe (sicherer + roher Lesezugriff) fuer t1, gefunden: ${JSON.stringify(prisma.__boundCallLog)}`, ).toBeGreaterThanOrEqual(2); }); it('Test 2: getConfigForApi eines Mandanten liefert NICHT die Konfiguration eines zweiten Mandanten, wenn beide im Speicher liegen', async () => { const prisma = makeFakePrisma(); prisma.__seedConfig('t1', { id: 'cfg-1', protocol: 'imap', isActive: true, host: 'mail-a.example.invalid', encryptedInboxCreds: null }); prisma.__seedConfig('t2', { id: 'cfg-2', protocol: 'imap', isActive: true, host: 'mail-b.example.invalid', encryptedInboxCreds: null }); const { service } = makeDkvService(prisma); const configForT1 = await service.getConfigForApi('t1'); expect(configForT1?.host).toBe('mail-a.example.invalid'); expect(configForT1?.tenantId).toBe('t1'); }); it('Test 3: saveConfig(tenantId, dto) mit gesetztem Benutzernamen und LEEREM Passwort — Lese- und Schreibzugriff sind gebunden, das gespeicherte Passwort bleibt unveraendert', async () => { const prisma = makeFakePrisma(); const crypto = makeFakeCrypto(); prisma.__seedConfig('t1', { id: 'cfg-1', protocol: 'imap', isActive: true, encryptedInboxCreds: crypto.encrypt(JSON.stringify({ username: 'alt-user', password: 'geheim-123' })), }); const { service } = makeDkvService(prisma, { encrypt: crypto.encrypt, decrypt: crypto.decrypt }); await service.saveConfig('t1', { protocol: 'imap', encryption: 'ssl-tls', username: 'neu-user', password: '', } as any); expectBoundCall(prisma, 't1', 'dkvModuleConfig', 'findUnique'); expectBoundCall(prisma, 't1', 'dkvModuleConfig', 'upsert'); const raw = await prisma.dkvModuleConfig.findUnique({ where: { tenantId: 't1' } }); const rawCreds = JSON.parse(crypto.decrypt(raw.encryptedInboxCreds)); expect(rawCreds.password).toBe('geheim-123'); expect(rawCreds.username).toBe('neu-user'); }); it('Test 4: testConnection(tenantId, dto) mit leerem Passwort — der Rueckgriff auf die gespeicherten Zugangsdaten steht gebunden im Protokoll', async () => { const prisma = makeFakePrisma(); const crypto = makeFakeCrypto(); prisma.__seedConfig('t1', { id: 'cfg-1', protocol: 'imap', isActive: true, encryptedInboxCreds: crypto.encrypt(JSON.stringify({ username: 'user-a', password: 'geheim-123' })), }); const { service } = makeDkvService(prisma, { encrypt: crypto.encrypt, decrypt: crypto.decrypt }); await service.testConnection('t1', { protocol: 'imap', encryption: 'ssl-tls', password: '', } as any); expectBoundCall(prisma, 't1', 'dkvModuleConfig', 'findUnique'); }); it('Test 5: die Verarbeitungsstrecke liest ihre Konfiguration gebunden; bei fehlender Konfiguration bricht sie wie bisher still ab', async () => { const prisma = makeFakePrisma(); // Kein __seedConfig fuer t1 — die Konfiguration fehlt bewusst. const { service } = makeDkvService(prisma); const result = await service.checkNow('t1'); expectBoundCall(prisma, 't1', 'dkvModuleConfig', 'findUnique'); // Verhalten bleibt unveraendert: kein Fehler, checkNow meldet 'ok' — // die Rechnungsverarbeitung stellt fuer diesen Mandanten still die // Arbeit ein (Befund K, Stelle 2), das wird hier nur festgeschrieben. expect(result.status).toBe('ok'); }); it('Test 6: der bewusst uebergreifende Planer-Startpfad steht NICHT im Bindungsprotokoll — Fehlen der Bindung ist hier die bestandene Erwartung, NICHT spaeter "reparieren"', async () => { const prisma = makeFakePrisma(); prisma.__seedConfig('t1', { id: 'cfg-1', protocol: 'imap', isActive: true, encryptedInboxCreds: 'enc(egal)' }); const { service } = makeDkvService(prisma); await service.loadAnyActiveConfigForScheduler(); expect( prisma.__boundCallLog.length, `der Planer-Startpfad darf KEINEN gebundenen Aufruf erzeugen, gefunden: ${JSON.stringify(prisma.__boundCallLog)}`, ).toBe(0); }); it('Test 7: der Planer-Startpfad liefert die verschluesselten Zugangsdaten NICHT mit (Befund D — Entlastung wird festgeschrieben, nicht geglaubt)', async () => { const prisma = makeFakePrisma(); prisma.__seedConfig('t1', { id: 'cfg-1', protocol: 'imap', isActive: true, encryptedInboxCreds: 'enc(sollte-nie-hier-auftauchen)', }); const { service } = makeDkvService(prisma); const result = await service.loadAnyActiveConfigForScheduler(); expect(result).not.toBeNull(); expect((result as any).encryptedInboxCreds).toBeUndefined(); }); // ─── Aufgabe 3 (260909-mir): dkvVehicleMaster / dkvInvoiceHistory / getExportFile ─── afterEach(() => { vi.restoreAllMocks(); }); it('Test 1: listVehicles/createVehicle stehen gebunden im Protokoll und liefern bzw. schreiben nur unter dem uebergebenen Mandanten', async () => { const prisma = makeFakePrisma(); prisma.__seedVehicle({ id: 'veh-a1', tenantId: 't1', kennzeichen: 'A-ONLY-1' }); prisma.__seedVehicle({ id: 'veh-b1', tenantId: 't2', kennzeichen: 'B-ONLY-1' }); const { service } = makeDkvService(prisma); const listed = await service.listVehicles('t1'); expect(listed).toHaveLength(1); expect(listed[0].tenantId).toBe('t1'); await service.createVehicle('t1', { kennzeichen: 'NEU-1', marke: 'X', modell: 'Y', fahrer: 'Z' } as any); expectBoundCall(prisma, 't1', 'dkvVehicleMaster', 'findMany'); expectBoundCall(prisma, 't1', 'dkvVehicleMaster', 'create'); const listedAgain = await service.listVehicles('t1'); expect(listedAgain).toHaveLength(2); expect(listedAgain.every((v: any) => v.tenantId === 't1')).toBe(true); }); it('Test 2: updateVehicle/deleteVehicle — BEIDE Anweisungen (Besitzpruefung und Schreibzugriff) stehen gebunden im Protokoll', async () => { const prisma = makeFakePrisma(); prisma.__seedVehicle({ id: 'veh-a1', tenantId: 't1', kennzeichen: 'A-ONLY-1' }); prisma.__seedVehicle({ id: 'veh-a2', tenantId: 't1', kennzeichen: 'A-ONLY-2' }); const { service } = makeDkvService(prisma); await service.updateVehicle('t1', 'veh-a1', { marke: 'Neu' } as any); expectBoundCall(prisma, 't1', 'dkvVehicleMaster', 'findFirst'); expectBoundCall(prisma, 't1', 'dkvVehicleMaster', 'update'); await service.deleteVehicle('t1', 'veh-a2'); const deleteFindFirstCalls = prisma.__boundCallLog.filter( (c: any) => c.tenantId === 't1' && c.model === 'dkvVehicleMaster' && c.method === 'findFirst', ); expect(deleteFindFirstCalls.length).toBeGreaterThanOrEqual(2); expectBoundCall(prisma, 't1', 'dkvVehicleMaster', 'delete'); }); it('Test 3: updateVehicle/deleteVehicle auf ein Fahrzeug eines FREMDEN Mandanten werfen weiterhin die vorhandene NotFoundException', async () => { const prisma = makeFakePrisma(); prisma.__seedVehicle({ id: 'veh-b1', tenantId: 't2', kennzeichen: 'B-ONLY-1' }); const { service } = makeDkvService(prisma); await expect(service.updateVehicle('t1', 'veh-b1', { marke: 'X' } as any)).rejects.toThrow('Vehicle not found'); await expect(service.deleteVehicle('t1', 'veh-b1')).rejects.toThrow('Vehicle not found'); }); it('Test 4: importVehiclesCsv in beiden Modi — jede Anweisung steht gebunden im Protokoll; der Ersetzen-Modus loescht ausschliesslich Fahrzeuge des eigenen Mandanten', async () => { const prisma = makeFakePrisma(); prisma.__seedVehicle({ id: 'veh-a1', tenantId: 't1', kennzeichen: 'ALT-1' }); prisma.__seedVehicle({ id: 'veh-b1', tenantId: 't2', kennzeichen: 'B-ONLY-1' }); const { service } = makeDkvService(prisma); const csv = 'Kennzeichen;Marke;Modell;Fahrer\nNEU-1;Marke;Modell;Fahrer'; await service.importVehiclesCsv('t1', csv, 'replace'); expectBoundCall(prisma, 't1', 'dkvVehicleMaster', 'deleteMany'); expectBoundCall(prisma, 't1', 'dkvVehicleMaster', 'createMany'); const t1Vehicles = await service.listVehicles('t1'); const t2Vehicles = await service.listVehicles('t2'); expect(t1Vehicles.map((v: any) => v.kennzeichen)).toEqual(['NEU-1']); expect(t2Vehicles).toHaveLength(1); // t2's vehicle survived the t1-scoped replace const csvMerge = 'Kennzeichen;Marke;Modell;Fahrer\nNEU-1;Marke2;Modell2;Fahrer2\nWEITERES-1;M;M;F'; await service.importVehiclesCsv('t1', csvMerge, 'merge'); expectBoundCall(prisma, 't1', 'dkvVehicleMaster', 'upsert'); }); it('Test 5: getHistory — beide parallel gestarteten Abfragen stehen gebunden im Protokoll, die Gesamtzahl zaehlt nur eigene Zeilen', async () => { const prisma = makeFakePrisma(); prisma.__seedHistory({ id: 'hist-a1', tenantId: 't1' }); prisma.__seedHistory({ id: 'hist-a2', tenantId: 't1' }); prisma.__seedHistory({ id: 'hist-b1', tenantId: 't2' }); const { service } = makeDkvService(prisma); const result = await service.getHistory('t1', 1, 20); expect(result.total).toBe(2); expect(result.items).toHaveLength(2); expectBoundCall(prisma, 't1', 'dkvInvoiceHistory', 'findMany'); expectBoundCall(prisma, 't1', 'dkvInvoiceHistory', 'count'); }); it('Test 6: die beiden Historien-Schreibzugriffe der Verarbeitungsstrecke (Erfolgsfall und Zerlegungsfehler) stehen gebunden im Protokoll', async () => { // Erfolgsfall const prismaOk = makeFakePrisma(); prismaOk.__seedConfig('t1', { id: 'cfg-1', protocol: 'imap', isActive: true, host: 'mail.example.invalid', folder: 'INBOX', encryptedInboxCreds: 'enc({"username":"u","password":"p"})', }); const email = { subject: 'RG', uid: 1, attachments: [{ buffer: Buffer.from('pdf-bytes') }] }; const parserOk = { parsePdf: vi.fn(async () => ({ vehicles: [], rechnungsnummer: 'RG-1', rechnungsdatum: '01.01.2026', })), }; const exporter = { buildExcelBuffer: vi.fn(() => Buffer.from('xlsx')), writeAndPrune: vi.fn(() => 'RG-DKV-RG-1-260101.xlsx'), resolveFahrzeug: vi.fn(() => ''), }; const imapProvider = { fetchPdfAttachments: vi.fn(async () => [email]) }; const { service: serviceOk } = makeDkvService(prismaOk, { parser: parserOk, exporter, imapProvider }); await serviceOk.checkNow('t1'); expectBoundCall(prismaOk, 't1', 'dkvInvoiceHistory', 'create'); // Zerlegungsfehler-Fall const prismaFail = makeFakePrisma(); prismaFail.__seedConfig('t1', { id: 'cfg-2', protocol: 'imap', isActive: true, host: 'mail.example.invalid', folder: 'INBOX', encryptedInboxCreds: 'enc({"username":"u","password":"p"})', }); const parserFail = { parsePdf: vi.fn(async () => { throw new Error('kaputt'); }) }; const imapProviderFail = { fetchPdfAttachments: vi.fn(async () => [email]) }; const { service: serviceFail } = makeDkvService(prismaFail, { parser: parserFail, exporter, imapProvider: imapProviderFail, }); await serviceFail.checkNow('t1'); expectBoundCall(prismaFail, 't1', 'dkvInvoiceHistory', 'create'); }); it('Test 7: der gebuendelte Lesezugriff auf die Fahrzeugstammdaten beim Aufbau der Ausfuhrzeilen steht gebunden im Protokoll und zieht keine Fahrzeuge eines zweiten Mandanten in die Ausfuhrdatei', async () => { const prisma = makeFakePrisma(); prisma.__seedConfig('t1', { id: 'cfg-1', protocol: 'imap', isActive: true, host: 'mail.example.invalid', folder: 'INBOX', encryptedInboxCreds: 'enc({"username":"u","password":"p"})', }); prisma.__seedVehicle({ id: 'veh-a1', tenantId: 't1', kennzeichen: 'A-1', marke: 'MarkeA', modell: 'ModellA', fahrer: 'FahrerA' }); prisma.__seedVehicle({ id: 'veh-b1', tenantId: 't2', kennzeichen: 'A-1', marke: 'FREMD', modell: 'FREMD', fahrer: 'FREMD-Fahrer' }); const email = { subject: 'RG', uid: 1, attachments: [{ buffer: Buffer.from('pdf-bytes') }] }; let capturedRows: any[] | null = null; const parser = { parsePdf: vi.fn(async () => ({ vehicles: [{ kennzeichen: 'A-1', transactions: [{ lieferdatum: '01.01.2026', ort: 'Ort', kilometerstand: 100 }] }], rechnungsnummer: 'RG-1', rechnungsdatum: '01.01.2026', })), }; const exporter = { buildExcelBuffer: vi.fn((rows: any[]) => { capturedRows = rows; return Buffer.from('xlsx'); }), writeAndPrune: vi.fn(() => 'RG-DKV-RG-1-260101.xlsx'), resolveFahrzeug: vi.fn((v: any) => `${v.marke}/${v.modell}/${v.kennzeichen}`), }; const imapProvider = { fetchPdfAttachments: vi.fn(async () => [email]) }; const { service } = makeDkvService(prisma, { parser, exporter, imapProvider }); await service.checkNow('t1'); expectBoundCall(prisma, 't1', 'dkvVehicleMaster', 'findMany'); expect(capturedRows).not.toBeNull(); expect(capturedRows![0].fahrer).toBe('FahrerA'); expect(capturedRows![0].fahrer).not.toBe('FREMD-Fahrer'); }); it('Test 8: getExportFile liefert eine Datei, zu der eine Historienzeile DIESES Mandanten mit passendem Dateinamen existiert', async () => { const prisma = makeFakePrisma(); prisma.__seedHistory({ id: 'hist-a1', tenantId: 't1', exportFilename: 'RG-DKV-TEST-A.xlsx' }); const { service } = makeDkvService(prisma); vi.mocked(fs.existsSync).mockReturnValue(true); vi.mocked(fs.readFileSync).mockReturnValue(Buffer.from('xlsx-bytes')); const buffer = await service.getExportFile('t1', 'RG-DKV-TEST-A.xlsx'); expect(buffer.toString()).toBe('xlsx-bytes'); }); it('Test 9: getExportFile verweigert dieselbe Datei einem ZWEITEN Mandanten mit der vorhandenen NotFoundException, obwohl die Datei existiert und das Namensmuster besteht (Befund E — die geschlossene Luecke)', async () => { const prisma = makeFakePrisma(); prisma.__seedHistory({ id: 'hist-a1', tenantId: 't1', exportFilename: 'RG-DKV-TEST-A.xlsx' }); const { service } = makeDkvService(prisma); vi.mocked(fs.existsSync).mockReturnValue(true); vi.mocked(fs.readFileSync).mockReturnValue(Buffer.from('xlsx-bytes')); await expect(service.getExportFile('t2', 'RG-DKV-TEST-A.xlsx')).rejects.toThrow( 'Export file not found: RG-DKV-TEST-A.xlsx', ); }); it('Test 10: der Riegel laeuft ueber einen GEBUNDENEN Lesezugriff auf die Historie — im Bindungsprotokoll nachweisbar, nicht nur am Ergebnis', async () => { const prisma = makeFakePrisma(); prisma.__seedHistory({ id: 'hist-a1', tenantId: 't1', exportFilename: 'RG-DKV-TEST-A.xlsx' }); const { service } = makeDkvService(prisma); vi.mocked(fs.existsSync).mockReturnValue(true); vi.mocked(fs.readFileSync).mockReturnValue(Buffer.from('xlsx-bytes')); await service.getExportFile('t1', 'RG-DKV-TEST-A.xlsx'); expectBoundCall(prisma, 't1', 'dkvInvoiceHistory', 'findFirst'); }); });