import { describe, expect, it, vi } from 'vitest'; import { TenderFingerprintBackfillService } from './tender-fingerprint-backfill.service'; import { tenderFingerprint } from './tender-fingerprint'; /** * TenderFingerprintBackfillService.spec — WINDOWS.md #11 Task 3. Hand- * rolled fake PrismaService (Map), matching the project convention * (tender-dedup.service.spec.ts, ldap-config.service.spec.ts). No real DB * I/O — the real-DB recompute is proven separately by the plan's verify * step against the local database. */ function isOutdated(fingerprint: string | null): boolean { return fingerprint === null || !fingerprint.startsWith('v2:'); } function makeFakePrisma(seedRows: any[]) { const tenders = new Map(seedRows.map((r) => [r.id, { ...r }])); const findManyOutdated = vi.fn(async ({ select, take }: any) => { const rows = [...tenders.values()].filter((t) => isOutdated(t.fingerprint)); const page = rows.slice(0, take); return page.map((row) => { const out: any = {}; for (const k of Object.keys(select)) out[k] = row[k]; return out; }); }); const update = vi.fn(async ({ where, data }: any) => { const existing = tenders.get(where.id); const updated = { ...existing, ...data }; tenders.set(where.id, updated); return updated; }); const transaction = vi.fn(async (ops: Promise[]) => Promise.all(ops)); const prisma = { tender: { findMany: findManyOutdated, update }, $transaction: transaction, __store: { tenders }, }; return prisma; } describe('TenderFingerprintBackfillService', () => { it('eine Datenbank ohne Zeilen loest keine Schreiboperation aus und der Haken wirft nicht', async () => { const prisma = makeFakePrisma([]); const service = new TenderFingerprintBackfillService(prisma as any); await expect(service.onApplicationBootstrap()).resolves.not.toThrow(); expect(prisma.tender.update).not.toHaveBeenCalled(); expect(prisma.$transaction).not.toHaveBeenCalled(); }); it('Zeilen mit altem Hash und Zeilen mit leerem Hash werden beide erfasst und tragen danach die neue Marke', async () => { const prisma = makeFakePrisma([ { id: 't-old', buyerName: 'Stadt Altstadt', title: 'Sanierung Rathaus', deadlineAt: null, fingerprint: 'oldhashvalue', }, { id: 't-null', buyerName: 'Stadt Neustadt', title: 'Neubau Schule', deadlineAt: null, fingerprint: null, }, { id: 't-current', buyerName: 'Stadt Gegenwart', title: 'Bereits aktuell', deadlineAt: null, fingerprint: 'v2:already-current', }, ]); const service = new TenderFingerprintBackfillService(prisma as any); const updated = await service.backfillOutdated(); expect(updated).toBe(2); const rowOld = prisma.__store.tenders.get('t-old'); const rowNull = prisma.__store.tenders.get('t-null'); const rowCurrent = prisma.__store.tenders.get('t-current'); expect(rowOld.fingerprint).toBe( tenderFingerprint({ buyerName: 'Stadt Altstadt', title: 'Sanierung Rathaus', deadlineAt: null }), ); expect(rowNull.fingerprint).toBe( tenderFingerprint({ buyerName: 'Stadt Neustadt', title: 'Neubau Schule', deadlineAt: null }), ); // Unangetastet — trug bereits die neue Marke. expect(rowCurrent.fingerprint).toBe('v2:already-current'); }); it('ein zweiter Lauf direkt danach schreibt nichts mehr', async () => { const prisma = makeFakePrisma([ { id: 't-old', buyerName: 'Stadt Altstadt', title: 'Sanierung Rathaus', deadlineAt: null, fingerprint: 'oldhashvalue', }, ]); const service = new TenderFingerprintBackfillService(prisma as any); const first = await service.backfillOutdated(); expect(first).toBe(1); prisma.tender.update.mockClear(); const second = await service.backfillOutdated(); expect(second).toBe(0); expect(prisma.tender.update).not.toHaveBeenCalled(); }); it('wirft die Datenbankabfrage, wirft der Haken trotzdem nicht', async () => { const prisma = { tender: { findMany: vi.fn().mockRejectedValue(new Error('DB weg')), update: vi.fn(), }, $transaction: vi.fn(), }; const service = new TenderFingerprintBackfillService(prisma as any); await expect(service.onApplicationBootstrap()).resolves.not.toThrow(); expect(prisma.tender.update).not.toHaveBeenCalled(); }); });