import { readdirSync, readFileSync } from 'node:fs'; import { join } from 'node:path'; import { describe, expect, it } from 'vitest'; /** * Prüft das hand-geschriebene Migrations-SQL für den Wechsel von * TenderRssFeedSource.url @unique auf @@unique([userId, url]) (Phase 17, * Plan 02, D-02) — reiner Textabgleich ohne Datenbank, gleiches Muster wie * doe-url-migration-sql.spec.ts / email-config-migration-sql.spec.ts. * * Anders als bei der TenderEmailConfig-Migration (Plan 17-01) gibt es hier * KEINEN Bestandsdaten-Umzug: eine bereits vorhandene Zeile bekommt keinen * Besitzer zugewiesen und bleibt damit plattformweit — genau der * Ist-Zustand (17-CONTEXT.md, offener Punkt 2). Das ist die zentrale * Korrektheitsbedingung dieser Migration, nicht eine Zuordnungsreihenfolge. */ const MIGRATIONS_DIR = join(__dirname, '../../prisma/migrations'); function readMigrationSql(suffix: string): string { const dirs = readdirSync(MIGRATIONS_DIR, { withFileTypes: true }) .filter((entry) => entry.isDirectory() && entry.name.endsWith(suffix)) .map((entry) => entry.name); if (dirs.length !== 1) { throw new Error( `Expected exactly one migration directory ending in "${suffix}", found ${dirs.length}: ${dirs.join(', ')}`, ); } return readFileSync(join(MIGRATIONS_DIR, dirs[0], 'migration.sql'), 'utf-8'); } describe('tender_rss_feed_owner migration.sql', () => { const sql = readMigrationSql('_tender_rss_feed_owner'); it('ergaenzt beide neuen Spalten ohne Pflichtwert', () => { expect(sql).toMatch(/ADD COLUMN\s+"userId"\s+TEXT(?!\s+NOT NULL)/); expect(sql).toMatch(/ADD COLUMN\s+"tenantId"\s+TEXT(?!\s+NOT NULL)/); // Weder Spalte traegt irgendwo in der Datei eine Pflicht-Klausel. expect(sql).not.toMatch(/ALTER COLUMN\s+"userId"\s+SET NOT NULL/); expect(sql).not.toMatch(/ALTER COLUMN\s+"tenantId"\s+SET NOT NULL/); }); it('hebt den alten eindeutigen Index auf der Adresse auf', () => { expect(sql).toContain('DROP INDEX "TenderRssFeedSource_url_key"'); }); it('legt einen neuen eindeutigen Index ueber Besitzer und Adresse an', () => { expect(sql).toContain( 'CREATE UNIQUE INDEX "TenderRssFeedSource_userId_url_key" ON "TenderRssFeedSource"("userId", "url")', ); }); it('legt einen gewoehnlichen Index auf dem Besitzer an', () => { expect(sql).toContain( 'CREATE INDEX "TenderRssFeedSource_userId_idx" ON "TenderRssFeedSource"("userId")', ); }); it('enthaelt keine Anweisung, die Bestandszeilen aendert oder loescht — anders als die TenderEmailConfig-Migration (Plan 17-01) gibt es hier keinen Datenumzug', () => { expect(sql).not.toMatch(/UPDATE\s+"TenderRssFeedSource"/); expect(sql).not.toMatch(/DELETE FROM\s+"TenderRssFeedSource"/); }); it('legt die neue Eindeutigkeitsregel erst NACH dem Entfernen der alten an (Reihenfolge)', () => { const dropIdx = sql.indexOf('DROP INDEX "TenderRssFeedSource_url_key"'); const createIdx = sql.indexOf('CREATE UNIQUE INDEX "TenderRssFeedSource_userId_url_key"'); expect(dropIdx).toBeGreaterThanOrEqual(0); expect(createIdx).toBeGreaterThan(dropIdx); }); });