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
This commit is contained in:
@@ -0,0 +1,73 @@
|
||||
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);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user