Przeglądaj źródła

SEC-1858: Added integration tests to validate that the xsd is documented in the reference

Rob Winch 13 lat temu
rodzic
commit
c8b847f1ed

+ 4 - 0
config/config.gradle

@@ -44,6 +44,10 @@ dependencies {
                 "cglib:cglib-nodep:2.2"
 }
 
+test {
+    inputs.file file("$rootDir/docs/manual/src/docbook/appendix-namespace.xml")
+}
+
 integrationTest {
     systemProperties['apacheDSWorkDir'] = "${buildDir}/apacheDSWork"
 }

+ 49 - 0
config/src/test/groovy/org/springframework/security/config/doc/Attribute.groovy

@@ -0,0 +1,49 @@
+/*
+ * Copyright 2011 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy of
+ * the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package org.springframework.security.config.doc
+
+/**
+ * Represents a Spring Security XSD Attribute. It is created when parsing the current xsd to compare to the documented appendix.
+ *
+ * @author Rob Winch
+ * @see SpringSecurityXsdParser
+ * @see XsdDocumentedSpec
+ */
+class Attribute {
+    def prefix = ""
+    def name
+    def desc
+    def elmt
+
+    def indent() {
+        prefix += "    "
+    }
+
+    def toDocbook(prefix) {
+        def indent = "    "+prefix
+        """
+${prefix}<section xml:id="${id}">
+${indent}<title><literal>${name}</literal></title>
+${indent}<para>${desc}</para>
+${prefix}</section>"""
+    }
+    def getId() {
+        return "${elmt.id}-${name}".toString()
+    }
+    public String toString() {
+        prefix + '@' + name + " - " + desc
+    }
+}

+ 164 - 0
config/src/test/groovy/org/springframework/security/config/doc/Element.groovy

@@ -0,0 +1,164 @@
+/*
+ * Copyright 2011 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy of
+ * the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package org.springframework.security.config.doc
+
+/**
+* Represents a Spring Security XSD Element. It is created when parsing the current xsd to compare to the documented appendix.
+*
+* @author Rob Winch
+* @see SpringSecurityXsdParser
+* @see XsdDocumentedSpec
+*/
+class Element {
+    def prefix = ""
+    def name
+    def desc
+    def attrs
+    /**
+     * Contains the elements that extend this element (i.e. any-user-service contains ldap-user-service)
+     */
+    def subGrps = []
+    def childElmts = [:]
+    def parentElmts = [:]
+
+    def indent() {
+        prefix += "    "
+        attrs*.indent()
+        childElmts.values()*.indent()
+    }
+
+    def getId() {
+        return "nsa-${name}".toString()
+    }
+
+    /**
+     * Gets all the ids related to this Element including attributes, parent elements, and child elements.
+     *
+     * <p>
+     * The expected ids to be found are documented below.
+     * <ul>
+     * <li>Elements - any xml element will have the nsa-&lt;element&gt;. For example the http element will have the id
+     * nsa-http</li>
+     * <li>Parent Section - Any element with a parent other than beans will have a section named
+     * nsa-&lt;element&gt;-parents. For example, authentication-provider would have a section id of
+     * nsa-authentication-provider-parents. The section would then contain a list of links pointing to the
+     * documentation for each parent element.</li>
+     * <li>Attributes Section - Any element with attributes will have a section with the id
+     * nsa-&lt;element&gt;-attributes. For example the http element would require a section with the id
+     * http-attributes.</li>
+     * <li>Attribute - Each attribute of an element would have an id of nsa-&lt;element&gt;-&lt;attributeName&gt;. For
+     * example the attribute create-session for the http attribute would have the id http-create-session.</li>
+     * <li>Child Section - Any element with a child element will have a section named nsa-&lt;element&gt;-children.
+     * For example, authentication-provider would have a section id of nsa-authentication-provider-children. The
+     * section would then contain a list of links pointing to the documentation for each child element.</li>
+     * </ul>
+     * @return
+     */
+    def getIds() {
+        def ids = [id]
+        childElmts.values()*.ids.each { ids.addAll it }
+        attrs*.id.each { ids.add it }
+        if(childElmts) {
+            ids.add id+'-children'
+        }
+        if(attrs) {
+            ids.add id+'-attributes'
+        }
+        if(parentElmts) {
+            ids.add id+'-parents'
+        }
+        ids
+    }
+
+    def getFullName() {
+        parentElmt ? parentElmt.fullName+"-"+name : name
+    }
+
+    def docbookParentElmts(prefix) {
+        if(parentElmts.empty) {
+            return ''
+        }
+        def indent = prefix+'    '
+        def parents = """
+${prefix}<section xml:id="${id}-parents">
+${indent}<title>Parent Elements of <literal>&lt;${name}&gt;</literal></title>
+${indent}<orderedlist>"""
+        parentElmts.sort {l,r -> l.name.compareTo(r.name)}.each {
+            parents += """\n${indent}    <listitem><link xlink:href="#${it.id}">${it.name}</link></listitem>"""
+        }
+        parents += "\n${indent}</orderedlist>\n${prefix}</section>"
+        parents
+    }
+
+    def docbookChildElmts(prefix) {
+        if(!childElmts) {
+            return ''
+        }
+        def indent = prefix+'    '
+        def children = """
+${prefix}<section xml:id="${id}-children">
+${indent}<title>Child Elements of <literal>&lt;${name}&gt;</literal></title>
+${indent}<orderedlist>"""
+        childElmts.values().sort {l,r -> l.name.compareTo(r.name)}.each {
+            children += """\n${indent}    <listitem><link xlink:href="#${it.id}">${it.name}</link></listitem>"""
+        }
+        children += "\n${indent}</orderedlist>\n${prefix}</section>"
+        children
+    }
+    def toDocbook(prefix) {
+        def indent = prefix+'    '
+        def parentElmt = docbookParentElmts(indent)
+        def attributes = ""
+        attrs.sort {l,r -> l.name.compareTo(r.name)}.each {
+            attributes += it.toDocbook(indent+'    ')
+        }
+        if(attributes) {
+            attributes = """
+${indent}<section xml:id="${id}-attributes">
+${indent}    <title><literal>&lt;${name}&gt;</literal> Attributes</title>${attributes}
+${indent}</section>"""
+        }
+        def childElmts = docbookChildElmts(indent)
+        def elements = ""
+        childElmts.values().sort {l,r -> l.name.compareTo(r.name)}.each {
+            elements += it.toDocbook(prefix)
+        }
+        """
+${prefix}<section xml:id="${id}">
+${indent}<title><literal>&lt;${name}&gt;</literal></title>
+${indent}<para>${desc}</para>${parentElmt}${attributes}${childElmts}
+${prefix}</section>${elements}"""
+    }
+
+    public String toString() {
+        def result = prefix + name + " - " + desc+"\n"
+        attrs.sort {l,r -> l.name.compareTo(r.name)}.each { result+= it.toString()}
+        childElmts.values().sort {l,r -> l.name.compareTo(r.name)}.each { result+= it.toString() }
+        result
+    }
+
+    def getAllChildElmts() {
+        def result = [:]
+        childElmts.values()*.subGrps*.each { elmt -> result.put(elmt.name,elmt) }
+        result + childElmts
+    }
+
+    def getAllParentElmts() {
+        def result = [:]
+        parentElmts.values()*.subGrps*.each { elmt -> result.put(elmt.name,elmt) }
+        result + parentElmts
+    }
+}

+ 179 - 0
config/src/test/groovy/org/springframework/security/config/doc/SpringSecurityXsdParser.groovy

@@ -0,0 +1,179 @@
+/*
+ * Copyright 2011 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy of
+ * the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package org.springframework.security.config.doc
+
+import groovy.xml.Namespace
+
+/**
+ * Parses the Spring Security Xsd Document
+ *
+ * @author Rob Winch
+ */
+class SpringSecurityXsdParser {
+    private def rootElement
+
+    private def xs = new Namespace("http://www.w3.org/2001/XMLSchema", 'xs')
+    private def attrElmts = [] as Set
+    private def elementNameToElement = [:] as Map
+
+    /**
+     * Returns a map of the element name to the {@link Element}.
+     * @return
+     */
+    Map<String,Element> parse() {
+        elements(rootElement)
+        elementNameToElement
+    }
+
+    /**
+     * Creates a Map of the name to an Element object of all the children of element.
+     *
+     * @param element
+     * @return
+     */
+    private def elements(element) {
+        def elementNameToElement = [:] as Map
+        element.children().each { c->
+            if(c.name() == 'element') {
+              def e = elmt(c)
+              elementNameToElement.put(e.name,e)
+            } else {
+              elementNameToElement.putAll(elements(c))
+            }
+        }
+        elementNameToElement
+    }
+
+    /**
+     * Any children that are attribute will be returned as an Attribute object.
+     * @param element
+     * @return a collection of Attribute objects that are children of element.
+     */
+    private def attrs(element) {
+        def r = []
+        element.children().each { c->
+            if(c.name() == 'attribute') {
+                r.add(attr(c))
+            }else if(c.name() == 'element') {
+            }else {
+                r.addAll(attrs(c))
+            }
+        }
+        r
+    }
+
+    /**
+     * Any children will be searched for an attributeGroup, each of it's children will be returned as an Attribute
+     * @param element
+     * @return
+     */
+    private def attrgrps(element) {
+        def r = []
+        element.children().each { c->
+            if(c.name() == 'element') {
+            }else if (c.name() == 'attributeGroup') {
+               if(c.attributes().get('name')) {
+                   r.addAll(attrgrp(c))
+               } else {
+                   private def n = c.attributes().get('ref').split(':')[1]
+                   private def attrGrp = findNode(element,n)
+                   r.addAll(attrgrp(attrGrp))
+               }
+            } else {
+               r.addAll(attrgrps(c))
+            }
+        }
+        r
+    }
+
+    private def findNode(c,name) {
+        def root = c
+        while(root.name() != 'schema') {
+            root = root.parent()
+        }
+        def result = root.breadthFirst().find { child-> name == child.@name?.text() }
+        assert result?.@name?.text() == name
+        result
+    }
+
+    /**
+     * Processes an individual attributeGroup by obtaining all the attributes and then looking for more attributeGroup elements and prcessing them.
+     * @param e
+     * @return all the attributes for a specific attributeGroup and any child attributeGroups
+     */
+    private def attrgrp(e) {
+        def attrs = attrs(e)
+        attrs.addAll(attrgrps(e))
+        attrs
+    }
+
+    /**
+     * Obtains the description for a specific element
+     * @param element
+     * @return
+     */
+    private def desc(element) {
+        return element['annotation']['documentation']
+    }
+
+    /**
+     * Given an element creates an attribute from it.
+     * @param n
+     * @return
+     */
+    private def attr(n) {
+        new Attribute(desc: desc(n), name: n.@name.text())
+    }
+
+    /**
+     * Given an element creates an Element out of it by collecting all its attributes and child elements.
+     *
+     * @param n
+     * @return
+     */
+    private def elmt(n) {
+        def name = n.@ref.text()
+        if(name) {
+            name = name.split(':')[1]
+            n = findNode(n,name)
+        } else {
+           name = n.@name.text()
+        }
+        if(elementNameToElement.containsKey(name)) {
+            return elementNameToElement.get(name)
+        }
+        attrElmts.add(name)
+        def e = new Element()
+        e.name = n.@name.text()
+        e.desc = desc(n)
+        e.childElmts = elements(n)
+        e.attrs = attrs(n)
+        e.attrs.addAll(attrgrps(n))
+        e.childElmts.values()*.indent()
+        e.attrs*.indent()
+        e.attrs*.elmt = e
+        e.childElmts.values()*.each { it.parentElmts.put(e.name,e) }
+
+        def subGrpName = n.@substitutionGroup.text()
+        if(subGrpName) {
+            def subGrp = elmt(findNode(n,subGrpName.split(":")[1]))
+            subGrp.subGrps.add(e)
+        }
+
+        elementNameToElement.put(name,e)
+        e
+    }
+}

+ 143 - 0
config/src/test/groovy/org/springframework/security/config/doc/XsdDocumentedSpec.groovy

@@ -0,0 +1,143 @@
+/*
+ * Copyright 2011 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy of
+ * the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package org.springframework.security.config.doc
+
+import groovy.util.slurpersupport.NodeChild;
+import spock.lang.*
+
+/**
+ * Tests to ensure that the xsd is properly documented.
+ *
+ * @author Rob Winch
+ */
+class XsdDocumentedSpec extends Specification {
+
+    def ignoredIds = ['nsa-any-user-service','nsa-any-user-service-parents','nsa-authentication','nsa-ldap','nsa-method-security','nsa-web']
+    @Shared def appendix = new File('../docs/manual/src/docbook/appendix-namespace.xml')
+    @Shared def appendixRoot = new XmlSlurper().parse(appendix)
+
+    @Shared File schemaDocument = new File('src/main/resources/org/springframework/security/config/spring-security-3.1.xsd')
+    @Shared Map<String,Element> elementNameToElement
+
+    def setupSpec() {
+        def rootElement = new XmlSlurper().parse(schemaDocument)
+        elementNameToElement = new SpringSecurityXsdParser(rootElement: rootElement).parse()
+        appendixRoot.getMetaClass().sections = {
+            delegate.breadthFirst().inject([]) {result, c->
+                if(c.name() == 'section' && c.@id) {
+                    result.add(c)
+                }
+                result
+            }
+        }
+        NodeChild.metaClass.hrefs = { result ->
+            def id = delegate.@id.text().replace('-parents', '').replace('-children', '')
+            result.put(id,[])
+            delegate.children().breadthFirst().each { sectionChild ->
+                def href = sectionChild.@href.text()
+                if(href) {
+                    result.get(id).add(href[1..-1])
+                }
+            }
+        }
+    }
+
+    /**
+     * This will check to ensure that the expected number of xsd documents are found to ensure that we are validating
+     * against the current xsd document. If this test fails, all that is needed is to update the schemaDocument
+     * and the expected size for this test.
+     * @return
+     */
+    def 'the latest schema is being validated'() {
+        when: 'all the schemas are found'
+        def schemas = schemaDocument.getParentFile().list().findAll { it.endsWith('.xsd') }
+        then: 'the count is equal to 7, if not then schemaDocument needs updated'
+        schemas.size() == 7
+    }
+
+    /**
+     * This uses a naming convention for the ids of the appendix to ensure that the entire appendix is documented.
+     * The naming convention for the ids is documented in {@link Element#getIds()}.
+     * @return
+     */
+    def 'the entire schema is included in the appendix documentation'() {
+        setup: 'get all the documented ids and the expected ids'
+        def documentedIds = appendixRoot.sections().collect { it.@id.text() }
+        when: 'the schema is compared to the appendix documentation'
+        def expectedIds = [] as Set
+        elementNameToElement*.value*.ids*.each { expectedIds.addAll it }
+        documentedIds.removeAll ignoredIds
+        expectedIds.removeAll ignoredIds
+        then: 'all the elements and attributes are documented'
+        documentedIds.sort() == expectedIds.sort()
+    }
+
+    /**
+     * This test ensures that any element that has children or parents contains a section that has links pointing to that
+     * documentation.
+     * @return
+     */
+    def 'validate parents and children are linked in the appendix documentation'() {
+        when: "get all the links for each element's children and parents"
+        def docAttrNameToChildren = [:]
+        def docAttrNameToParents = [:]
+        appendixRoot.sections().each { c->
+            def id = c.@id.text()
+            if(id.endsWith('-parents')) {
+                c.hrefs(docAttrNameToParents)
+            }
+            if(id.endsWith('-children')) {
+                c.hrefs(docAttrNameToChildren)
+            }
+        }
+        def schemaAttrNameToParents = [:]
+        def schemaAttrNameToChildren = [:]
+        elementNameToElement.each { entry ->
+            def key = 'nsa-'+entry.key
+            if(ignoredIds.contains(key)) {
+                return
+            }
+            def parentIds = entry.value.allParentElmts.values()*.id.findAll { !ignoredIds.contains(it) }.sort()
+            if(parentIds) {
+                schemaAttrNameToParents.put(key,parentIds)
+            }
+            def childIds = entry.value.allChildElmts.values()*.id.findAll { !ignoredIds.contains(it) }.sort()
+            if(childIds) {
+                schemaAttrNameToChildren.put(key,childIds)
+            }
+        }
+        then: "the expected parents and children are all documented"
+        schemaAttrNameToChildren.sort() == docAttrNameToChildren.sort()
+        schemaAttrNameToParents.sort() == docAttrNameToParents.sort()
+    }
+
+    /**
+     * This test checks each xsd element and ensures there is documentation for it.
+     * @return
+     */
+    def 'entire xsd is documented'() {
+        when: "validate that the entire xsd contains documentation"
+        def notDocElmtIds = elementNameToElement.values().findAll {
+            !it.desc.text() && !ignoredIds.contains(it.id)
+        }*.id.sort().join("\n")
+        def notDocAttrIds = elementNameToElement.values()*.attrs.flatten().findAll {
+            !it.desc.text() && !ignoredIds.contains(it.id)
+        }*.id.sort().join("\n")
+        then: "all the elements and attributes have some documentation"
+        !notDocElmtIds
+        !notDocAttrIds
+    }
+}