test(13-02): add failing SourceRegistry denylist-gate spec

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) <noreply@anthropic.com>
This commit is contained in:
2026-07-23 08:42:51 +02:00
parent 1b11ada112
commit 78b17ef28b
@@ -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<RawTenderRecord[]> {
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();
});
});