// @vitest-environment jsdom import { describe, expect, it } from 'vitest'; import { importFile } from '../../src/importers/importer'; import { importSource } from '../../src/importers/engine'; import { importProfiles } from '../../src/profiles/profile-catalog'; import { nodeImportRuntimeAdapters } from '../../server/node-import-runtime-adapters'; import { createCanonicalImportBatch } from '../../src/importers/canonical-import'; import { confirmEntryMappings, previewEntryFromDataset } from '../../src/features/import-workflow/import-workflow'; import { assertCanonicalAssessmentModel } from '../../src/domain/canonical-validation'; import { decodeHtmlDocument, decodeHtmlLexically, markupText, sanitizeHtmlText } from '../../src/importers/html-extraction'; import { protectHtmlAttributeMarkup } from '../../src/importers/html-attribute-markup'; const html = (body: string) => `PingCastle Healthcheck${body}`; const heading = (matched: string, total = '5') => `

Stale Objects rule details [${matched} rules matched on a total of ${total}]

`; const rule = (id: string, extra = '') => `

Synthetic finding

Rule ID:

${id}

${extra}
`; const browserImport = (text: string, extension: string) => importFile(new File([text], `ad_hc_synthetic.${extension}`), { profiles: importProfiles }); function serverImport(text: string, extension: string) { const bytes = new TextEncoder().encode(text); return importSource({ metadata: { name: `ad_hc_synthetic.${extension}`, sizeBytes: bytes.length, lastModified: 0 }, bytes }, { profiles: importProfiles }, nodeImportRuntimeAdapters); } async function both(text: string, extension: string) { return [await browserImport(text, extension), await serverImport(text, extension)]; } function canonical(dataset: Awaited>) { const preview = previewEntryFromDataset(dataset, { fileName: dataset.fileName, sha256: 'a'.repeat(64), sizeBytes: dataset.fileSize, lastModified: 0 }); const model = createCanonicalImportBatch('pingcastle-integrity', [confirmEntryMappings(preview, importProfiles)], '2026-09-09T12:00:00Z').model; assertCanonicalAssessmentModel(model); return model; } describe('PingCastle context and finding integrity', () => { it('preserves text, every exact XML attribute, namespaced attributes and explicit empty leaves in both runtimes', async () => { const source = `0 Synthetic ]]>`; const results = await both(source, 'xml'); expect(results[0].extraction).toEqual(results[1].extraction); for (const dataset of results) { const model = canonical(dataset); const observations = model.environmentObservations ?? []; expect(model.findingOccurrences).toHaveLength(0); expect(observations).toContainEqual(expect.objectContaining({ originalValue: null, state: 'empty', source: expect.objectContaining({ objectPath: 'HealthcheckData/EmptyCollection[3]' }) })); expect(observations).toContainEqual(expect.objectContaining({ originalValue: '0', normalizedValue: 0, state: 'observed', source: expect.objectContaining({ objectPath: 'HealthcheckData/Rights[5]/Right[1]/@Count' }) })); expect(observations).toContainEqual(expect.objectContaining({ originalValue: 'false', normalizedValue: false, source: expect.objectContaining({ objectPath: 'HealthcheckData/Rights[5]/Right[1]/@test:allow' }) })); expect(observations).toContainEqual(expect.objectContaining({ originalValue: '', state: 'empty', source: expect.objectContaining({ objectPath: 'HealthcheckData/Rights[5]/Right[1]/@blank' }) })); expect(observations).toContainEqual(expect.objectContaining({ originalValue: 'a "; b < c & d', source: expect.objectContaining({ objectPath: 'HealthcheckData/Text[4]/@quote' }) })); expect(observations).toContainEqual(expect.objectContaining({ originalValue: 'retained', source: expect.objectContaining({ objectPath: 'HealthcheckData/Rights[5]/Right[1]/@__proto__' }) })); expect(observations).toContainEqual(expect.objectContaining({ originalValue: 'Synthetic ' })); expect(observations.some(item => /RiskRules/.test(item.source.objectPath ?? ''))).toBe(false); expect(model.reportSections?.[0].tables[0].rows).toHaveLength(observations.length); expect(dataset.rawText).toBe(source); } }); it.each(['html', 'xml'])('rejects a missing native ID in %s instead of discarding a rule', async extension => { const source = extension === 'html' ? html(heading('1') + rule('')) : '10'; for (const run of [browserImport, serverImport]) await expect(run(source, extension)).rejects.toMatchObject({ diagnostics: expect.arrayContaining([expect.objectContaining({ code: 'pingcastle-rule-id-missing', severity: 'error' })]) }); }); it.each(['html', 'xml'])('diagnoses duplicate IDs in %s without changing the existing first-presentation behavior', async extension => { const source = extension === 'html' ? html(heading('1') + rule('S-Synthetic') + rule('S-Synthetic')) : 'S-SyntheticS-Synthetic'; for (const dataset of await both(source, extension)) { expect(dataset.extraction?.candidates).toHaveLength(1); expect(dataset.extraction?.processing.status).toBe('partial'); expect(dataset.extraction?.diagnostics).toContainEqual(expect.objectContaining({ code: 'pingcastle-duplicate-rule-id', source: expect.objectContaining({ objectPath: expect.any(String) }) })); } }); it('warns on mismatched totals, preserving all valid rules and the original declaration', async () => { for (const dataset of await both(html(heading('1') + rule('S-One') + rule('S-Two')), 'html')) { expect(dataset.extraction?.candidates).toHaveLength(2); expect(dataset.extraction?.diagnostics).toContainEqual(expect.objectContaining({ code: 'pingcastle-rule-count-mismatch', details: { category: 'Stale Objects', declared: 1, actual: 2 } })); const model = canonical(dataset); expect(model.findingOccurrences).toHaveLength(2); expect(model.reportSections?.find(section => section.sectionKey === 'pingcastle-rule-coverage')?.tables[0].rows[0].slice(1, 4)).toEqual(['1', '5', 2]); } }); it('accepts an explicitly empty report but rejects an announced nonempty report without results', async () => { for (const dataset of await both(html(heading('0')), 'html')) { expect(dataset.extraction?.candidates).toHaveLength(0); expect(dataset.extraction?.processing.status).toBe('complete'); } for (const run of [browserImport, serverImport]) await expect(run(html(heading('2')), 'html')).rejects.toMatchObject({ diagnostics: expect.arrayContaining([expect.objectContaining({ code: 'pingcastle-rule-results-missing' })]) }); }); it.each(['NaN', '-1', '9007199254740992', '6'])('diagnoses an invalid declared count %s', async count => { const dataset = await browserImport(html(heading(count) + rule('S-Synthetic')), 'html'); expect(dataset.extraction?.diagnostics).toContainEqual(expect.objectContaining({ code: 'pingcastle-rule-count-invalid' })); }); it('keeps categories independent and does not confuse the catalog size with matched results', async () => { const body = heading('1', '56') + rule('S-One') + '

Anomalies rule details [0 rules matched on a total of 73]

'; const dataset = await browserImport(html(body), 'html'); expect(dataset.extraction?.diagnostics).toEqual([]); expect(dataset.extraction?.candidates).toHaveLength(1); }); it('keeps tooltip markup out of contextual cells, direct finding details and linked detail tables', async () => { const table = `
NameCount
Synthetic value0
`; const source = html(heading('1') + rule('S-Synthetic', 'Details:

Synthetic details

' + table) + '

Domain Information

' + table + '
'); for (const dataset of await both(source, 'html')) { const model = canonical(dataset); const context = model.reportSections?.find(section => section.sectionKey === 'sectionDomainInformation')!; expect(context.tables[0].rows).toEqual([['Synthetic value', '0']]); const candidate = dataset.extraction!.candidates[0]; expect(candidate.fields.find(field => field.label === 'Details')?.decodedValue).toBe('Synthetic details\nName | Count\nSynthetic value | 0'); expect(candidate.fields.find(field => field.key === 'linked-section')?.originalValue).toMatchObject({ sections: [{ rows: [['Synthetic value', '0']] }] }); expect(JSON.stringify(model.environmentObservations)).not.toMatch(/Tooltip|Hidden|data-bs-content|technical/); expect(dataset.rawText).toBe(source); } }); it('keeps later report sections when a bounded metadata excerpt ends within an attribute', async () => { const source = html(`
${'x'.repeat(499_900)}
Visible` + heading('1') + rule('S-Synthetic') + '

Domain Information

Value
Synthetic
'); for (const dataset of await both(source, 'html')) { expect(dataset.extraction?.candidates).toHaveLength(1); expect(dataset.extraction?.reportSections?.find(section => section.sectionKey === 'sectionDomainInformation')?.tables[0].rows).toEqual([['Synthetic']]); expect(dataset.rawText).toBe(source); } }); }); describe('bounded quoted-attribute markup handling', () => { it.each(['"', "'"])('agrees with the DOM parser for %s-quoted tooltips', quote => { const source = `
Value
not visible

${quote} rowspan="1">42 & safe
`; const document = new DOMParser().parseFromString(source, 'text/html'); // The DOM path deliberately keeps safe presentation markup; compare visible values. expect(decodeHtmlLexically(source).tables.map(table => table.rows)).toEqual(decodeHtmlDocument(document).tables.map(table => table.rows.map(row => row.map(markupText)))); expect(markupText(source)).toBe('Value 42 & safe'); }); it('retains values, entities and raw script strings without interpreting them', () => { const source = '

Safe

'; const protectedText = protectHtmlAttributeMarkup(source); expect(protectedText).toContain('const text = `

{ expect(() => protectHtmlAttributeMarkup('
hidden">Visible

hidden">Visible

Next

`; expect(markupText(source)).toBe('Visible Next'); expect(protectHtmlAttributeMarkup(source)).toContain(`data-label=it's title="<b>hidden</b>"`); }); it('does not sanitize attribute-like strings inside other attributes or visible prose', () => { const source = `

Visible src='text'

Link`; const sanitized = sanitizeHtmlText(source); expect(sanitized.removedContentCount).toBe(2); expect(markupText(sanitized.text)).toBe("Visible src='text' Link"); expect(protectHtmlAttributeMarkup(sanitized.text)).toBe(sanitized.text); }); });