import dedent from 'dedent'; import { XMLParser } from 'fast-xml-parser'; import { afterEach, describe, expect, it, vi } from 'vitest'; import { containsXml, validateXml } from '../../src/assertions/xml'; afterEach(() => { vi.restoreAllMocks(); }); describe('validateXml', () => { it('should validate a simple valid XML string', () => { expect(validateXml('Content')).toEqual({ isValid: true, reason: 'XML is valid and contains all required elements', }); }); it('should invalidate a malformed XML string', () => { expect(validateXml('Content { for (const valid of [ '', ' \n', '\uFEFF', '\n', '', '<𐀀>astral name', '', '<𐀀/>', ]) { expect(validateXml(valid)).toEqual({ isValid: true, reason: 'XML is valid and contains all required elements', }); } }); it('should invalidate non-well-formed XML the lenient parser accepts', () => { for (const malformed of [ '', // mismatched tags '', // improperly nested 'textmore', // multiple root elements 'x', // unquoted attribute value '', // multiple self-closing root elements 'trailing', // text outside the root element '', // invalid comment body '', // duplicate attributes '', // unbound namespace prefix '', // invalid reserved namespace binding '', // duplicate expanded attributes '&undefined;', // undeclared entity '&undefined;', // bare doctype cannot declare entities '\u0001', // disallowed XML character '', // missing root element '""', // quoted XML output '```xml\n\n```', // fenced XML output ]) { expect(validateXml(malformed)).toEqual({ isValid: false, reason: expect.stringContaining('XML parsing failed'), }); } }); it('should reject DTD internal subsets that cannot be fully validated', () => { for (const xml of [ ']>&writer;', ']>&logo;', '">]>&bad;', ']>', ]) { // A DTD internal subset is a deliberate policy rejection, so the reason // is reported verbatim rather than wrapped as an "XML parsing failed" error. expect(validateXml(xml)).toEqual({ isValid: false, reason: 'DTD internal subsets are not supported by is-xml validation', }); } }); it('should accept external DOCTYPEs whose quoted system identifiers contain brackets', () => { // The `[` lives inside a quoted SYSTEM literal, so it must not be // mistaken for the start of an internal subset and rejected. for (const xml of [ '', '', ]) { expect(validateXml(xml)).toEqual({ isValid: true, reason: 'XML is valid and contains all required elements', }); } }); it('should accept external DOCTYPE entity references without loading external resources', () => { for (const xml of [ ' ', '&external;', ]) { expect(validateXml(xml)).toEqual({ isValid: true, reason: 'XML is valid and contains all required elements', }); } }); it('should reject undeclared external entities in standalone documents', () => { expect( validateXml( '&external;', ), ).toEqual({ isValid: false, reason: expect.stringContaining('XML parsing failed'), }); }); it('should reject malformed external DOCTYPE declarations', () => { for (const xml of [ '', '', '', '', '', '', '', '', '', '', '', ]) { expect(validateXml(xml)).toEqual({ isValid: false, reason: 'XML parsing failed: Malformed DOCTYPE declaration', }); } }); it('should validate XML with attributes', () => { expect(validateXml('Content')).toEqual({ isValid: true, reason: 'XML is valid and contains all required elements', }); }); it('should validate XML with namespaces', () => { expect( validateXml('Content'), ).toEqual({ isValid: true, reason: 'XML is valid and contains all required elements', }); }); it('should validate when all required elements are present', () => { expect( validateXml( 'T-shirtRed', ['analysis.classification', 'analysis.color'], ), ).toEqual({ isValid: true, reason: 'XML is valid and contains all required elements', }); }); it('should invalidate when a required element is missing', () => { expect( validateXml('T-shirt', [ 'analysis.classification', 'analysis.color', ]), ).toEqual({ isValid: false, reason: 'XML is missing required elements: analysis.color', }); }); it('should validate nested elements correctly', () => { expect( validateXml('Content', [ 'root.parent.child.grandchild', ]), ).toEqual({ isValid: true, reason: 'XML is valid and contains all required elements', }); }); it('should invalidate when a nested required element is missing', () => { expect( validateXml('', [ 'root.parent.child.grandchild', ]), ).toEqual({ isValid: false, reason: 'XML is missing required elements: root.parent.child.grandchild', }); }); it('should handle empty elements correctly', () => { expect( validateXml('Content', [ 'root.emptyChild', 'root.nonEmptyChild', ]), ).toEqual({ isValid: true, reason: 'XML is valid and contains all required elements', }); }); it('should validate XML with multiple siblings', () => { expect( validateXml('Content1Content2', ['root.child']), ).toEqual({ isValid: true, reason: 'XML is valid and contains all required elements', }); }); it('should validate indexed required elements within repeated siblings', () => { expect( validateXml('FirstSecond', [ 'root.item.0.name', 'root.item.1.name', ]), ).toEqual({ isValid: true, reason: 'XML is valid and contains all required elements', }); }); it('should handle XML with CDATA sections', () => { expect( validateXml('This is CDATA content

]]>
', [ 'root.child', ]), ).toEqual({ isValid: true, reason: 'XML is valid and contains all required elements', }); }); it('should validate XML with processing instructions', () => { const xml = 'Content'; expect(validateXml(xml, ['root.child'])).toEqual({ isValid: true, reason: 'XML is valid and contains all required elements', }); }); it('should handle XML with comments', () => { expect( validateXml('Content', ['root.child']), ).toEqual({ isValid: true, reason: 'XML is valid and contains all required elements', }); }); it('should validate the example XML structure', () => { const xml = dedent` T-shirt/top White with black print Large circular graphic design on the front, resembling a smiley face or emoji 9 The image clearly shows a short-sleeved garment with a round neckline, which is characteristic of a T-shirt. The large circular graphic on the front is distinctive and appears to be a stylized smiley face or emoji design, which is popular in contemporary casual fashion. The stark contrast between the white fabric and black print is very clear, leaving little room for misinterpretation. The style is unmistakably modern and aligned with current trends in graphic tees. My confidence is high (9) because all elements of the image are clear and consistent with a typical graphic T-shirt design. `; expect( validateXml(xml, [ 'analysis.classification', 'analysis.color', 'analysis.features', 'analysis.style', 'analysis.confidence', 'analysis.reasoning', ]), ).toEqual({ isValid: true, reason: 'XML is valid and contains all required elements', }); }); it('should reject the example XML structure when a required element is missing', () => { const xml = dedent` T-shirt/top White with black print 9 The image clearly shows a short-sleeved garment with a round neckline, which is characteristic of a T-shirt. `; expect( validateXml(xml, [ 'analysis.classification', 'analysis.color', 'analysis.features', 'analysis.style', 'analysis.confidence', 'analysis.reasoning', ]), ).toEqual({ isValid: false, reason: 'XML is missing required elements: analysis.features', }); }); }); describe('containsXml', () => { it('should return true when valid XML is present', () => { const input = 'Some text Content more text'; const result = containsXml(input); expect(result.isValid).toBe(true); }); it('should return false when no XML is present', () => { const input = 'This is just plain text'; expect(containsXml(input)).toEqual({ isValid: false, reason: 'No XML content found in the output', }); }); it('should validate required elements', () => { const input = 'Text Content more'; const result = containsXml(input, ['root.child']); expect(result.isValid).toBe(true); }); it('should validate indexed required elements within extracted XML', () => { const input = 'Text FirstSecond more'; const result = containsXml(input, ['root.item.1.name']); expect(result.isValid).toBe(true); }); it('should return false when required elements are missing', () => { const input = 'Text Content more'; expect(containsXml(input, ['root.missing'])).toEqual({ isValid: false, reason: 'No valid XML content found matching the requirements', }); }); it('should handle multiple XML fragments', () => { const input = 'Content text More'; const result = containsXml(input, ['root2.child']); expect(result.isValid).toBe(true); }); it('should validate requirements across multiple XML fragments', () => { const input = 'content1 more text content2'; const result = containsXml(input, ['xml1', 'xml2']); expect(result.isValid).toBe(true); }); it('should find valid XML after earlier unclosed tags', () => { const input = 'Text Content'; const result = containsXml(input, ['root.child']); expect(result.isValid).toBe(true); }); it('should find valid XML with non-ASCII element names', () => { const input = 'Text <élément>Content more'; expect(containsXml(input, ['élément'])).toEqual({ isValid: true, reason: 'XML is valid and contains all required elements', }); }); it('should find valid XML with non-ASCII text content', () => { const input = 'Text Café 東京 مرحبا more'; expect(containsXml(input, ['root.child'])).toEqual({ isValid: true, reason: 'XML is valid and contains all required elements', }); }); it('should find valid XML with non-ASCII attribute values', () => { const input = 'Text Content more'; expect(containsXml(input, ['root.child.@_label'])).toEqual({ isValid: true, reason: 'XML is valid and contains all required elements', }); }); it('should ignore pseudo-closing tags inside comments when extracting candidates', () => { const input = 'Content'; expect(containsXml(input, ['root.child'])).toEqual({ isValid: true, reason: 'XML is valid and contains all required elements', }); }); it('should ignore pseudo-closing tags inside CDATA when extracting candidates', () => { const input = ']]>Content'; expect(containsXml(input, ['root.child'])).toEqual({ isValid: true, reason: 'XML is valid and contains all required elements', }); }); it('should keep required elements root-relative within a valid candidate', () => { const input = 'Text Content'; expect(containsXml(input, ['root.child'])).toEqual({ isValid: false, reason: 'No valid XML content found matching the requirements', }); }); it('should not parse every nested candidate when required elements are missing', () => { const parseSpy = vi.spyOn(XMLParser.prototype, 'parse'); const input = `${'
'.repeat(1000)}${''.repeat(1000)}`; expect(containsXml(input, ['a.missing'])).toEqual({ isValid: false, reason: 'No valid XML content found matching the requirements', }); expect(parseSpy).toHaveBeenCalledTimes(1); }); it('should reject malformed opening tag streams without catastrophic backtracking', () => { const input = ' { const input = '