From c8b847f1ed1e92ad3442e0684f5701c7f1ec67d6 Mon Sep 17 00:00:00 2001 From: Rob Winch Date: Fri, 11 Nov 2011 09:00:53 -0500 Subject: [PATCH] SEC-1858: Added integration tests to validate that the xsd is documented in the reference --- config/config.gradle | 4 + .../security/config/doc/Attribute.groovy | 49 +++++ .../security/config/doc/Element.groovy | 164 ++++++++++++++++ .../config/doc/SpringSecurityXsdParser.groovy | 179 ++++++++++++++++++ .../config/doc/XsdDocumentedSpec.groovy | 143 ++++++++++++++ 5 files changed, 539 insertions(+) create mode 100644 config/src/test/groovy/org/springframework/security/config/doc/Attribute.groovy create mode 100644 config/src/test/groovy/org/springframework/security/config/doc/Element.groovy create mode 100644 config/src/test/groovy/org/springframework/security/config/doc/SpringSecurityXsdParser.groovy create mode 100644 config/src/test/groovy/org/springframework/security/config/doc/XsdDocumentedSpec.groovy diff --git a/config/config.gradle b/config/config.gradle index 0b5b43475a..fd9a9f0f4d 100644 --- a/config/config.gradle +++ b/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" } diff --git a/config/src/test/groovy/org/springframework/security/config/doc/Attribute.groovy b/config/src/test/groovy/org/springframework/security/config/doc/Attribute.groovy new file mode 100644 index 0000000000..398e543d69 --- /dev/null +++ b/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}
+${indent}<literal>${name}</literal> +${indent}${desc} +${prefix}
""" + } + def getId() { + return "${elmt.id}-${name}".toString() + } + public String toString() { + prefix + '@' + name + " - " + desc + } +} diff --git a/config/src/test/groovy/org/springframework/security/config/doc/Element.groovy b/config/src/test/groovy/org/springframework/security/config/doc/Element.groovy new file mode 100644 index 0000000000..95120da58c --- /dev/null +++ b/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. + * + *

+ * The expected ids to be found are documented below. + *

+ * @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}
+${indent}Parent Elements of <literal><${name}></literal> +${indent}""" + parentElmts.sort {l,r -> l.name.compareTo(r.name)}.each { + parents += """\n${indent} ${it.name}""" + } + parents += "\n${indent}\n${prefix}
" + parents + } + + def docbookChildElmts(prefix) { + if(!childElmts) { + return '' + } + def indent = prefix+' ' + def children = """ +${prefix}
+${indent}Child Elements of <literal><${name}></literal> +${indent}""" + childElmts.values().sort {l,r -> l.name.compareTo(r.name)}.each { + children += """\n${indent} ${it.name}""" + } + children += "\n${indent}\n${prefix}
" + 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}
+${indent} <literal><${name}></literal> Attributes${attributes} +${indent}
""" + } + def childElmts = docbookChildElmts(indent) + def elements = "" + childElmts.values().sort {l,r -> l.name.compareTo(r.name)}.each { + elements += it.toDocbook(prefix) + } + """ +${prefix}
+${indent}<literal><${name}></literal> +${indent}${desc}${parentElmt}${attributes}${childElmts} +${prefix}
${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 + } +} diff --git a/config/src/test/groovy/org/springframework/security/config/doc/SpringSecurityXsdParser.groovy b/config/src/test/groovy/org/springframework/security/config/doc/SpringSecurityXsdParser.groovy new file mode 100644 index 0000000000..6c5a081a44 --- /dev/null +++ b/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 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 + } +} diff --git a/config/src/test/groovy/org/springframework/security/config/doc/XsdDocumentedSpec.groovy b/config/src/test/groovy/org/springframework/security/config/doc/XsdDocumentedSpec.groovy new file mode 100644 index 0000000000..6f5f9673eb --- /dev/null +++ b/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 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 + } +}