feat(14-04): denylisted-portals read endpoint sourced from DENYLISTED_PORTALS

- Add PORTAL_URLS map (vergabe24, aumass) in source-registry.ts, keyed off
  the existing DENYLISTED_PORTALS constant so the portal set is never
  re-declared
- Add GET /modules/tender-radar/denylisted-portals, declared before
  @Get(':id') (route-order pitfall), mapping over DENYLISTED_PORTALS
- Extend tenders.controller.spec.ts: response shape + route-order guard

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-07-23 13:58:25 +02:00
parent c140d1186a
commit 28c6c7fee1
3 changed files with 72 additions and 0 deletions
@@ -315,6 +315,16 @@ describe('TendersController — route declaration order (static route before :id
expect(coverageIdx).toBeLessThan(idIdx);
});
it('declares getDenylistedPortals before getTender so GET /:id cannot shadow "denylisted-portals" (Plan 14-04, Pitfall 5)', () => {
const methods = Object.getOwnPropertyNames(TendersController.prototype);
const denylistIdx = methods.indexOf('getDenylistedPortals');
const idIdx = methods.indexOf('getTender');
expect(denylistIdx).toBeGreaterThanOrEqual(0);
expect(idIdx).toBeGreaterThanOrEqual(0);
expect(denylistIdx).toBeLessThan(idIdx);
});
it('declares listRssFeeds/createRssFeed/removeRssFeed before getTender so GET /:id cannot shadow "rss-feeds" (Plan 14-02, Pitfall 5)', () => {
const methods = Object.getOwnPropertyNames(TendersController.prototype);
const listIdx = methods.indexOf('listRssFeeds');
@@ -624,6 +634,31 @@ describe('TendersController — GET /coverage', () => {
});
});
describe('TendersController — GET /denylisted-portals (Plan 14-04, UI-06/D-12)', () => {
it('returns vergabe24 and aumass each with their canonical URL, derived from DENYLISTED_PORTALS', async () => {
const prisma = makeFakePrisma();
const scheduler = { setInterval: vi.fn(), stopJob: vi.fn() } as any;
const controller = new TendersController(
prisma as any,
scheduler,
makeFakeTriageService() as any,
makeFakeSavedSearchService() as any,
makeFakeNotificationPrefService() as any,
makeFakeRssFeedService() as any,
makeFakeEmailConfigService() as any,
);
const result = await controller.getDenylistedPortals();
expect(result).toEqual({
portals: [
{ portal: 'vergabe24', url: 'https://www.vergabe24.de' },
{ portal: 'aumass', url: 'https://www.aumass.de' },
],
});
});
});
describe('TendersController — GET /triage (batch, per-user, T-11-10/11)', () => {
it('parses the comma-separated ids param and delegates to tenderTriage.listForUser(userId, ids)', async () => {
const prisma = makeFakePrisma();