diff --git a/Tests/ValidationRuleTests.swift b/Tests/ValidationRuleTests.swift new file mode 100644 index 0000000..3e8dd48 --- /dev/null +++ b/Tests/ValidationRuleTests.swift @@ -0,0 +1,111 @@ +@testable import PureXML +import Testing + +/// Per-rule isolation tests in the OpenAPIKit style: each builtin validation is +/// installed alone on a `blank` validator, run against a seeded subject, and the +/// resulting errors are asserted by count, reason, and coding path. +@Suite("Validation rules in isolation") +struct ValidationRuleTests { + private typealias DTDSchema = PureXML.Validation.DTDSchema + private typealias XSDContext = PureXML.Validation.XSDContext + + private func dtd(_ xml: String) throws -> (PureXML.Model.Node, DTDSchema) { + let parsed = try PureXML.Parsing.Parser().parseWithDocumentType(xml, limits: .init(allowDoctype: true)) + return (parsed.node, DTDSchema(parsed.documentType)) + } + + private func path(_ error: PureXML.Validation.ValidationError) -> [String] { + error.codingPath.map(\.stringValue) + } + + // MARK: Structural + + @Test("Structural.uniqueAttributes reports each duplicate at the element path") + func test_uniqueAttributes() { + let element = PureXML.Model.Element("a", attributes: [.init("x", "1"), .init("x", "2")]) + let node = PureXML.Model.Node.document([.element(element)]) + let errors = PureXML.Validation.Validator.blank + .validating(PureXML.Validation.Structural.uniqueAttributes) + .errors(for: node, in: ()) + #expect(errors.count == 1) + #expect(errors.first?.reason == "Duplicate attribute 'x' on ") + #expect(path(errors[0]) == ["a"]) + } + + // MARK: DTD + + @Test("DTD.contentModel reports an EMPTY element with content at the element path") + func test_contentModel() throws { + let (node, schema) = try dtd("]>") + let errors = PureXML.Validation.Validator.blank + .validating(PureXML.Validation.DTD.contentModel) + .errors(for: node, in: schema) + #expect(errors.count == 1) + #expect(errors.first?.reason == "element is declared EMPTY but has content") + #expect(path(errors[0]) == ["r"]) + } + + @Test("DTD.attributeDeclarations reports a missing required attribute") + func test_attributeDeclarations() throws { + let (node, schema) = try dtd("]>") + let errors = PureXML.Validation.Validator.blank + .validating(PureXML.Validation.DTD.attributeDeclarations) + .errors(for: node, in: schema) + #expect(errors.count == 1) + #expect(errors.first?.reason == "required attribute 'id' is missing on ") + #expect(path(errors[0]) == ["r"]) + } + + @Test("DTD.undeclaredElement uses the single-error Bool form (Failed to satisfy)") + func test_undeclaredElement() throws { + let (node, schema) = try dtd("]>") + let errors = PureXML.Validation.Validator.blank + .validating(PureXML.Validation.DTD.undeclaredElement) + .errors(for: node, in: schema) + #expect(errors.count == 1) + #expect(errors.first?.reason == "Failed to satisfy: Element is declared in the DTD") + #expect(path(errors[0]) == ["r"]) + } + + @Test("DTD.identifierIntegrity reports a duplicate ID at the document root") + func test_identifierIntegrity() throws { + let (node, schema) = try dtd("]>") + let errors = PureXML.Validation.Validator.blank + .validating(PureXML.Validation.DTD.identifierIntegrity) + .errors(for: node, in: schema) + #expect(errors.count == 1) + #expect(errors.first?.reason == "duplicate ID 'x' (declared 2 times)") + // Runs once over the whole tree, so it is located at the document root. + #expect(path(errors[0]).isEmpty) + #expect(String(describing: errors[0]).hasSuffix("at root of document")) + } + + // MARK: XSD + + @Test("XSD.contentValidity locates a simple-type violation at the root element") + func test_xsdContentValidity() throws { + let xsd = """ + + + + """ + let compiled = try PureXML.Schema.XSDParser.parse(xsd) + let node = try PureXML.parse("x") + guard case let .document(children) = node, let root = children.compactMap(\.element).first else { + Issue.record("no root element") + return + } + let context = XSDContext( + types: compiled.types, + constraints: compiled.constraints, + rootDeclaration: compiled.elements["n"], + ) + // The validator is rooted at the document element, as Schema.Document does. + let errors = PureXML.Validation.Validator.blank + .validating(PureXML.Validation.XSD.contentValidity) + .errors(for: .element(root), in: context) + #expect(errors.count == 1) + #expect(errors.first?.reason.contains("n") == true) + #expect(errors.first.map(path) == ["n"]) + } +}