Files
tessera-ctl/apps/api/src/tenders/rss-feed-migration-sql.spec.ts
T
schalli 7dee116254 test(17-02): D-06 ownerTenantId tagging for RSS ingestion + seed idempotency
- RssAdapter.fetchTenders tags every record from a feed with a tenantId
  with the same D-13 ownerTenantId origin marking email-alert records
  carry since Phase 14; platform-wide feeds (no tenantId) stay unmarked.
  The pure parseFeed mapping is untouched — tagging happens in the
  fan-out loop that knows which row a batch came from
- Extracted the service.bund.de seed out of TendersModule.onModuleInit
  into seedServiceBundRssFeed() (tenders.seed.ts, same pattern as the
  existing seedTendersModule), so the find-then-create idempotency added
  in Task 1 is unit-tested directly instead of only via a Nest bootstrap
- New rss-feed-migration-sql.spec.ts: text-only check of the Task 1
  migration file (nullable columns, dropped/created indexes, no
  existing-row mutation, correct ordering)

- Files modified: apps/api/src/tenders/adapters/rss.adapter.ts, apps/api/src/tenders/tenders.module.ts, apps/api/src/tenders/tenders.seed.ts, apps/api/src/tenders/adapters/rss.adapter.spec.ts, apps/api/src/tenders/tenders.seed.spec.ts, apps/api/src/tenders/rss-feed-migration-sql.spec.ts
2026-08-12 11:45:27 +02:00

74 lines
3.1 KiB
TypeScript

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);
});
});