PureXML/Tests/DTDAttributeTests.swift
Mihaela Mihaljevic 89f8ccc95a refactor(validation): migrate DTD validation onto the framework
Slice 3 of #92. The DTD validator's imperative walk becomes declared Validation
values over a DTDSchema document:

- DTDSchema is now just the schema (content models + attribute declarations) and
  serves as the rules' Document context; its validate/walk methods are gone.
- contentModel and attributeDeclarations are Validation<Element, DTDSchema>, so
  every content-model and attribute violation is now located by coding path
  ("... at path: r/a"). undeclaredElement is added only in strict mode.
- identifierIntegrity is a Validation<Node, DTDSchema> gated to the root, doing
  the document-wide ID-uniqueness and IDREF-resolution checks in one pass.
- DTD.validator(strict:) assembles the rule set; validateAgainstInternalDTD runs
  it and returns [ValidationError] instead of [Issue].

A new test pins the located result. Schema (XSD) and Schematron validators still
use Issue and migrate in the following slices.
2026-06-08 12:36:56 +02:00

47 lines
1.9 KiB
Swift

@testable import PureXML
import Testing
@Suite("DTD attribute validation")
struct DTDAttributeTests {
private func issues(_ xml: String, strict: Bool = false) throws -> [PureXML.Validation.ValidationError] {
try PureXML.validateAgainstInternalDTD(xml, strict: strict)
}
@Test("#REQUIRED attribute must be present")
func test_required() throws {
let dtd = "<!DOCTYPE r [<!ELEMENT r EMPTY><!ATTLIST r id CDATA #REQUIRED>]>"
try #expect(issues("\(dtd)<r id=\"x\"/>").isEmpty)
try #expect(!issues("\(dtd)<r/>").isEmpty)
}
@Test("#FIXED attribute must match when present and may be omitted")
func test_fixed() throws {
let dtd = "<!DOCTYPE r [<!ELEMENT r EMPTY><!ATTLIST r v CDATA #FIXED \"1\">]>"
try #expect(issues("\(dtd)<r v=\"1\"/>").isEmpty)
try #expect(issues("\(dtd)<r/>").isEmpty)
try #expect(!issues("\(dtd)<r v=\"2\"/>").isEmpty)
}
@Test("Enumerated attribute must take a listed value")
func test_enumeration() throws {
let dtd = "<!DOCTYPE r [<!ELEMENT r EMPTY><!ATTLIST r dir (ltr|rtl) #IMPLIED>]>"
try #expect(issues("\(dtd)<r dir=\"ltr\"/>").isEmpty)
try #expect(issues("\(dtd)<r/>").isEmpty)
try #expect(!issues("\(dtd)<r dir=\"up\"/>").isEmpty)
}
@Test("Multiple attributes in one ATTLIST are each checked")
func test_multipleAttributes() throws {
let dtd = "<!DOCTYPE r [<!ELEMENT r EMPTY><!ATTLIST r a CDATA #REQUIRED b (x|y) #IMPLIED>]>"
try #expect(issues("\(dtd)<r a=\"1\" b=\"x\"/>").isEmpty)
// a is missing and b is out of its enumeration: two violations.
try #expect(issues("\(dtd)<r b=\"z\"/>").count == 2)
}
@Test("Tokenized types (ID) do not constrain the value")
func test_idTypeUnconstrained() throws {
let dtd = "<!DOCTYPE r [<!ELEMENT r EMPTY><!ATTLIST r id ID #REQUIRED>]>"
try #expect(issues("\(dtd)<r id=\"anything\"/>").isEmpty)
}
}