ecf7872730
The doe-opendata adapter stored the OCDS document's own `uri` as sourceUrl. That is the API address of the record and serves OCDS JSON by design, so anyone following the link from the results list, the detail view, or an alert mail landed on raw JSON instead of the notice. Build the human-readable page from the notice id the row already carries instead. `/ui/de/search/details?noticeId=...` is the redirect target of `/ui/de/notices/...`, so it needs no redirect. Verified in a browser for both id shapes the feed uses -- numeric (25673764 -> "Feuerwehr-Geraetehaus Miehlen Fliesenarbeiten") and UUID (7085ba12-... -> "Holzfassade"). The page is a single-page app that answers 200 with an identical shell for any id, so this had to be checked on rendered content; a status code proves nothing. The adapter alone only fixes new ingests, so a backfill migration rewrites the rows already stored -- in Tender and in TenderSource, since the detail view lists per-source links separately. It touches only rows still pointing at /api/notices/ and only ids of a shape that was actually verified, which makes it idempotent and keeps an unexpected id from being pasted into a URL. Counted read-only against the live database beforehand: 2846 DOE rows affected, none skipped. Closes the 2026-08-05 backlog item, which was deliberately held until Phase 16 was done. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
69 lines
2.5 KiB
TypeScript
69 lines
2.5 KiB
TypeScript
import { readdirSync, readFileSync } from 'node:fs';
|
|
import { join } from 'node:path';
|
|
import { describe, expect, it } from 'vitest';
|
|
|
|
/**
|
|
* Prüft das hand-geschriebene Backfill-SQL für die DÖE-Bekanntmachungslinks
|
|
* ohne Datenbank — reiner Textabgleich, gleiches Muster wie
|
|
* groups/migration-sql.spec.ts.
|
|
*
|
|
* Der Adapter allein repariert nur neue Importe. Ohne den Nachlauf behalten
|
|
* die bereits importierten Ausschreibungen ihre kaputte API-URL, und genau
|
|
* die sieht der Nutzer heute in Trefferliste, Detailansicht und Alarm-Mail.
|
|
*/
|
|
|
|
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('doe_notice_url_backfill migration.sql', () => {
|
|
const sql = readMigrationSql('_doe_notice_url_backfill');
|
|
|
|
it('schreibt die Bekanntmachungsseite, nicht die API-Adresse', () => {
|
|
expect(sql).toContain(
|
|
"'https://oeffentlichevergabe.de/ui/de/search/details?noticeId=' || \"sourceNoticeId\"",
|
|
);
|
|
expect(sql).not.toMatch(/SET\s+"sourceUrl"\s*=\s*'[^']*\/api\/notices\//);
|
|
});
|
|
|
|
it('fasst beide Tabellen an — sonst bleiben die Quell-Links der zusammengefuehrten Tender kaputt', () => {
|
|
expect(sql).toMatch(/UPDATE\s+"Tender"/);
|
|
expect(sql).toMatch(/UPDATE\s+"TenderSource"/);
|
|
});
|
|
|
|
it('ist idempotent: nur Zeilen, die noch auf die API zeigen', () => {
|
|
const updates = sql.match(/UPDATE\s+"[A-Za-z]+"[\s\S]*?;/g) ?? [];
|
|
expect(updates.length).toBe(2);
|
|
for (const stmt of updates) {
|
|
expect(stmt).toContain(
|
|
`"sourceUrl" LIKE 'https://oeffentlichevergabe.de/api/notices/%'`,
|
|
);
|
|
expect(stmt).toContain(`"sourcePortal" = 'doe-opendata'`);
|
|
}
|
|
});
|
|
|
|
it('baut keine URL aus einer Kennung unerwarteter Form', () => {
|
|
const updates = sql.match(/UPDATE\s+"[A-Za-z]+"[\s\S]*?;/g) ?? [];
|
|
for (const stmt of updates) {
|
|
expect(stmt).toContain(`"sourceNoticeId" ~ '^[A-Za-z0-9-]+$'`);
|
|
}
|
|
});
|
|
|
|
it('fasst ausschliesslich DÖE-Zeilen an — andere Portale behalten ihre Links', () => {
|
|
expect(sql).not.toMatch(/UPDATE[\s\S]*?service-bund/);
|
|
expect(sql).not.toMatch(/UPDATE[\s\S]*?cosinex/);
|
|
});
|
|
});
|