test(quick-260723-e7i): cover ai-netserver/cosinex bag normalization + DOE regression
- bagRecord() helper builds inline RawTenderRecords for the flat ocdsPayload bag shape (no fixtures exist for NetServer/cosinex-DTVP) - ai-netserver and cosinex-dtvp full-bag mapping, null/empty-field fallback, and contentHash-format assertions - Existing DOE fixture-based assertions untouched, confirming the refactor didn't change DOE-path behavior Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -3,7 +3,7 @@ import { XMLParser } from 'fast-xml-parser';
|
||||
import { readFileSync } from 'fs';
|
||||
import { join } from 'path';
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import type { RawTenderRecord } from './tender.types';
|
||||
import type { RawTenderRecord, SourceType } from './tender.types';
|
||||
import { TenderNormalizerService } from './tender-normalizer.service';
|
||||
|
||||
/**
|
||||
@@ -65,6 +65,36 @@ const NOTICE_WITH_EFORMS_ONLY_DEADLINE =
|
||||
const NOTICE_WITH_NO_DEADLINE_OR_VALUE =
|
||||
'001c68dd-258a-4bf7-a264-478604ad9c3e-01';
|
||||
|
||||
/**
|
||||
* Builds a RawTenderRecord for the flat ocdsPayload-bag scraper adapters
|
||||
* (NetServer/cosinex-DTVP) — no fixture ZIPs exist for these sources, so
|
||||
* the record is constructed inline (gap closure, 13-VERIFICATION.md).
|
||||
*/
|
||||
function bagRecord(
|
||||
sourceType: SourceType,
|
||||
bag: {
|
||||
title?: string | null;
|
||||
buyerName?: string | null;
|
||||
procedureType?: string | null;
|
||||
legalFramework?: string | null;
|
||||
deadlineAt?: string | null;
|
||||
},
|
||||
): RawTenderRecord {
|
||||
const sourcePortal =
|
||||
sourceType === 'cosinex-dtvp' ? 'cosinex-dtvp' : 'tender24';
|
||||
return {
|
||||
sourceType,
|
||||
sourcePortal,
|
||||
sourceNoticeId: 'bag-notice-1',
|
||||
ocid: undefined,
|
||||
sourceUrl: `https://example.invalid/${sourcePortal}`,
|
||||
fetchedAt: new Date(),
|
||||
publishedAt: null,
|
||||
eformsPayload: null,
|
||||
ocdsPayload: bag,
|
||||
};
|
||||
}
|
||||
|
||||
describe('TenderNormalizerService', () => {
|
||||
const service = new TenderNormalizerService();
|
||||
|
||||
@@ -210,4 +240,81 @@ describe('TenderNormalizerService', () => {
|
||||
|
||||
expect(mutated.contentHash).not.toBe(original.contentHash);
|
||||
});
|
||||
|
||||
describe('generic ocdsPayload bag (ai-netserver / cosinex-dtvp gap closure)', () => {
|
||||
const fullBag = {
|
||||
title: 'Bauleistung X',
|
||||
buyerName: 'Stadt Y',
|
||||
procedureType: 'Offenes Verfahren',
|
||||
legalFramework: 'VOB/A',
|
||||
deadlineAt: '2026-09-01T10:00:00.000Z',
|
||||
};
|
||||
|
||||
it('ai-netserver: maps the flat bag into NormalizedTenderFields', () => {
|
||||
const raw = bagRecord('ai-netserver', fullBag);
|
||||
|
||||
const normalized = service.normalize(raw);
|
||||
|
||||
expect(normalized.title).toBe('Bauleistung X');
|
||||
expect(normalized.buyerName).toBe('Stadt Y');
|
||||
expect(normalized.procedureType).toBe('Offenes Verfahren');
|
||||
expect(normalized.deadlineAt).toBeInstanceOf(Date);
|
||||
expect(normalized.deadlineAt?.toISOString()).toBe(
|
||||
'2026-09-01T10:00:00.000Z',
|
||||
);
|
||||
expect(normalized.cpvCodes).toEqual([]);
|
||||
expect(normalized.cpvDivisions).toEqual([]);
|
||||
expect(normalized.region).toBeNull();
|
||||
expect(normalized.plz).toBeNull();
|
||||
expect(normalized.bundesland).toBeNull();
|
||||
expect(normalized.estimatedValue).toBeNull();
|
||||
expect(normalized.dedupKey).toBe(`${raw.sourcePortal}:${raw.sourceNoticeId}`);
|
||||
expect(normalized.status).toBe('active');
|
||||
});
|
||||
|
||||
it('cosinex-dtvp: maps an identical bag shape identically', () => {
|
||||
const raw = bagRecord('cosinex-dtvp', fullBag);
|
||||
|
||||
const normalized = service.normalize(raw);
|
||||
|
||||
expect(normalized.title).toBe('Bauleistung X');
|
||||
expect(normalized.buyerName).toBe('Stadt Y');
|
||||
expect(normalized.procedureType).toBe('Offenes Verfahren');
|
||||
expect(normalized.deadlineAt?.toISOString()).toBe(
|
||||
'2026-09-01T10:00:00.000Z',
|
||||
);
|
||||
expect(normalized.cpvCodes).toEqual([]);
|
||||
expect(normalized.cpvDivisions).toEqual([]);
|
||||
expect(normalized.region).toBeNull();
|
||||
expect(normalized.plz).toBeNull();
|
||||
expect(normalized.bundesland).toBeNull();
|
||||
expect(normalized.estimatedValue).toBeNull();
|
||||
expect(normalized.dedupKey).toBe(`${raw.sourcePortal}:${raw.sourceNoticeId}`);
|
||||
});
|
||||
|
||||
it('null/empty bag fields: buyerName/procedureType/deadlineAt stay null, title falls back', () => {
|
||||
const raw = bagRecord('ai-netserver', {
|
||||
title: '',
|
||||
buyerName: null,
|
||||
procedureType: null,
|
||||
legalFramework: null,
|
||||
deadlineAt: null,
|
||||
});
|
||||
|
||||
const normalized = service.normalize(raw);
|
||||
|
||||
expect(normalized.title).toBe('Unbenannte Ausschreibung');
|
||||
expect(normalized.buyerName).toBeNull();
|
||||
expect(normalized.procedureType).toBeNull();
|
||||
expect(normalized.deadlineAt).toBeNull();
|
||||
});
|
||||
|
||||
it('contentHash for a bag source is a stable sha256 (proves shared assemble() reuse)', () => {
|
||||
const raw = bagRecord('ai-netserver', fullBag);
|
||||
|
||||
const normalized = service.normalize(raw);
|
||||
|
||||
expect(normalized.contentHash).toMatch(/^[a-f0-9]{64}$/);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user