From 78b17ef28b927727b7d073b85dffe210c58a06ab Mon Sep 17 00:00:00 2001 From: Schalli Date: Thu, 23 Jul 2026 08:42:51 +0200 Subject: [PATCH] test(13-02): add failing SourceRegistry denylist-gate spec MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit RED — proves Erfolgskriterium 4 (INGEST-07): registering an adapter whose portals include vergabe24 or aumass must throw DeniedPortalError, including a mixed portals array with one denylisted entry. Also covers legitimate register/get/activeAdapters happy paths. Fake adapter stub, no real scraping. Co-Authored-By: Claude Opus 4.8 (1M context) --- apps/api/src/tenders/source-registry.spec.ts | 73 ++++++++++++++++++++ 1 file changed, 73 insertions(+) create mode 100644 apps/api/src/tenders/source-registry.spec.ts diff --git a/apps/api/src/tenders/source-registry.spec.ts b/apps/api/src/tenders/source-registry.spec.ts new file mode 100644 index 0000000..172a4a2 --- /dev/null +++ b/apps/api/src/tenders/source-registry.spec.ts @@ -0,0 +1,73 @@ +import { describe, expect, it } from 'vitest'; +import type { RawTenderRecord, SourceType } from './tender.types'; +import type { TenderSourceAdapter } from './adapters/tender-source-adapter.interface'; +import { DeniedPortalError, SourceRegistry } from './source-registry'; + +/** + * Fake adapter stub — no real scraping/HTTP, just satisfies the + * TenderSourceAdapter contract for registry-behavior tests. + */ +function fakeAdapter( + sourceType: SourceType, + portals: readonly string[], +): TenderSourceAdapter { + return { + sourceType, + portals, + async fetchTenders(_dayCursor: string): Promise { + return []; + }, + }; +} + +describe('SourceRegistry', () => { + it('throws DeniedPortalError when registering an adapter serving vergabe24', () => { + const registry = new SourceRegistry(); + const adapter = fakeAdapter('ai-netserver', ['vergabe24']); + + expect(() => registry.register(adapter)).toThrow(DeniedPortalError); + }); + + it('throws DeniedPortalError when registering an adapter serving aumass', () => { + const registry = new SourceRegistry(); + const adapter = fakeAdapter('cosinex-dtvp', ['aumass']); + + expect(() => registry.register(adapter)).toThrow(DeniedPortalError); + }); + + it('rejects a mixed portals array wholesale when one entry is denylisted', () => { + const registry = new SourceRegistry(); + const adapter = fakeAdapter('ai-netserver', ['tender24', 'aumass']); + + expect(() => registry.register(adapter)).toThrow(DeniedPortalError); + expect(registry.get('ai-netserver')).toBeUndefined(); + }); + + it('registers a legitimate adapter and returns it via get()', () => { + const registry = new SourceRegistry(); + const adapter = fakeAdapter('ai-netserver', ['tender24']); + + expect(() => registry.register(adapter)).not.toThrow(); + expect(registry.get('ai-netserver')).toBe(adapter); + }); + + it('activeAdapters() returns all registered adapters', () => { + const registry = new SourceRegistry(); + const doe = fakeAdapter('doe-opendata', ['doe-opendata']); + const netserver = fakeAdapter('ai-netserver', ['tender24']); + + registry.register(doe); + registry.register(netserver); + + expect(registry.activeAdapters()).toHaveLength(2); + expect(registry.activeAdapters()).toEqual( + expect.arrayContaining([doe, netserver]), + ); + }); + + it('get() returns undefined for an unregistered sourceType (no throw)', () => { + const registry = new SourceRegistry(); + + expect(registry.get('cosinex-dtvp')).toBeUndefined(); + }); +});