7dee116254
- 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
74 lines
3.1 KiB
TypeScript
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);
|
|
});
|
|
});
|