Skip to content

Commit 6f4a329

Browse files
committed
test: pin published taglib invocation contracts
1 parent fe2a5fc commit 6f4a329

3 files changed

Lines changed: 79 additions & 27 deletions

File tree

‎jakarta/src/test/java/org/owasp/encoder/tag/TaglibDescriptorTest.java‎

Lines changed: 28 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,9 @@
3737
import java.io.InputStream;
3838
import java.lang.reflect.Method;
3939
import java.lang.reflect.Modifier;
40+
import java.util.ArrayList;
4041
import java.util.LinkedHashMap;
42+
import java.util.List;
4143
import java.util.Map;
4244
import java.util.Arrays;
4345
import java.util.Set;
@@ -89,6 +91,7 @@ static Taglib load(String resource) throws Exception {
8991
Element e = (Element) n;
9092
if ("tag".equals(e.getLocalName())) {
9193
String name = child(e, "name");
94+
assertTagInvocationContract(resource + ": " + name, e);
9295
assertNull(resource + ": duplicate tag " + name,
9396
taglib.tagClasses.put(name, child(e, "tag-class")));
9497
} else if ("function".equals(e.getLocalName())) {
@@ -124,18 +127,35 @@ public void testAdvancedMatchesSupportedFacade() throws Exception {
124127
assertEquals("advanced functions", expected, advanced.functionSignatures.keySet());
125128
}
126129

127-
/**
128-
* Returns the trimmed text of the first direct child element with the
129-
* given local name.
130-
*/
131-
private static String child(Element parent, String localName) {
130+
/** Tags accept a required String value, including runtime EL expressions. */
131+
private static void assertTagInvocationContract(String context, Element tag) {
132+
assertEquals(context + " body-content", "empty", child(tag, "body-content"));
133+
List<Element> attributes = children(tag, "attribute");
134+
assertEquals(context + " attribute count", 1, attributes.size());
135+
Element value = attributes.get(0);
136+
assertEquals(context + " attribute name", "value", child(value, "name"));
137+
assertEquals(context + " value required", "true", child(value, "required"));
138+
assertEquals(context + " value rtexprvalue", "true", child(value, "rtexprvalue"));
139+
assertEquals(context + " value type", "java.lang.String", child(value, "type"));
140+
}
141+
142+
/** Returns the direct child elements with the given local name. */
143+
private static List<Element> children(Element parent, String localName) {
144+
List<Element> children = new ArrayList<Element>();
132145
for (Node n = parent.getFirstChild(); n != null; n = n.getNextSibling()) {
133146
if (n instanceof Element && localName.equals(n.getLocalName())) {
134-
return n.getTextContent().trim();
147+
children.add((Element) n);
135148
}
136149
}
137-
fail("missing <" + localName + ">");
138-
return null;
150+
return children;
151+
}
152+
153+
/** Returns the trimmed text of a required, non-duplicated direct child. */
154+
private static String child(Element parent, String localName) {
155+
List<Element> children = children(parent, localName);
156+
assertEquals(parent.getLocalName() + " must contain exactly one <" + localName + ">",
157+
1, children.size());
158+
return children.get(0).getTextContent().trim();
139159
}
140160

141161
private static void assertExposesForJson(String resource) throws Exception {

‎jsp/src/test/java/org/owasp/encoder/tag/TaglibDescriptorTest.java‎

Lines changed: 28 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,9 @@
3737
import java.io.InputStream;
3838
import java.lang.reflect.Method;
3939
import java.lang.reflect.Modifier;
40+
import java.util.ArrayList;
4041
import java.util.LinkedHashMap;
42+
import java.util.List;
4143
import java.util.Map;
4244
import java.util.Arrays;
4345
import java.util.Set;
@@ -89,6 +91,7 @@ static Taglib load(String resource) throws Exception {
8991
Element e = (Element) n;
9092
if ("tag".equals(e.getLocalName())) {
9193
String name = child(e, "name");
94+
assertTagInvocationContract(resource + ": " + name, e);
9295
assertNull(resource + ": duplicate tag " + name,
9396
taglib.tagClasses.put(name, child(e, "tag-class")));
9497
} else if ("function".equals(e.getLocalName())) {
@@ -124,18 +127,35 @@ public void testAdvancedMatchesSupportedFacade() throws Exception {
124127
assertEquals("advanced functions", expected, advanced.functionSignatures.keySet());
125128
}
126129

127-
/**
128-
* Returns the trimmed text of the first direct child element with the
129-
* given local name.
130-
*/
131-
private static String child(Element parent, String localName) {
130+
/** Tags accept a required String value, including runtime EL expressions. */
131+
private static void assertTagInvocationContract(String context, Element tag) {
132+
assertEquals(context + " body-content", "empty", child(tag, "body-content"));
133+
List<Element> attributes = children(tag, "attribute");
134+
assertEquals(context + " attribute count", 1, attributes.size());
135+
Element value = attributes.get(0);
136+
assertEquals(context + " attribute name", "value", child(value, "name"));
137+
assertEquals(context + " value required", "true", child(value, "required"));
138+
assertEquals(context + " value rtexprvalue", "true", child(value, "rtexprvalue"));
139+
assertEquals(context + " value type", "java.lang.String", child(value, "type"));
140+
}
141+
142+
/** Returns the direct child elements with the given local name. */
143+
private static List<Element> children(Element parent, String localName) {
144+
List<Element> children = new ArrayList<Element>();
132145
for (Node n = parent.getFirstChild(); n != null; n = n.getNextSibling()) {
133146
if (n instanceof Element && localName.equals(n.getLocalName())) {
134-
return n.getTextContent().trim();
147+
children.add((Element) n);
135148
}
136149
}
137-
fail("missing <" + localName + ">");
138-
return null;
150+
return children;
151+
}
152+
153+
/** Returns the trimmed text of a required, non-duplicated direct child. */
154+
private static String child(Element parent, String localName) {
155+
List<Element> children = children(parent, localName);
156+
assertEquals(parent.getLocalName() + " must contain exactly one <" + localName + ">",
157+
1, children.size());
158+
return children.get(0).getTextContent().trim();
139159
}
140160

141161
private static void assertExposesForJson(String resource) throws Exception {

‎scripts/check-taglib-parity.py‎

Lines changed: 23 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,10 @@
2222
"main/resources/META-INF/java-encoder-advanced.tld": (
2323
"https://www.owasp.org/index.php/OWASP_Java_Encoder_Project#advanced", "owasp.encoder.jakarta.advanced"),
2424
}
25+
TLD_SHORT_NAMES = {
26+
"main/resources/META-INF/java-encoder.tld": ("java-encoder", "e"),
27+
"main/resources/META-INF/java-encoder-advanced.tld": ("java-encoder", "java-encoder"),
28+
}
2529
CONSUMERS = "test/modules/owasp.encoder.jsp.consumer/"
2630

2731

@@ -34,6 +38,17 @@ def target_path(path):
3438

3539
def descriptor(text, path, translate):
3640
root = ET.fromstring(text)
41+
namespace = JAVAX_XML if translate else JAKARTA_XML
42+
family = "jsp" if translate else "jakarta"
43+
index = 0 if translate else 1
44+
# Pin the published identities before normalization: coordinated edits to
45+
# both families must not silently redefine a user's taglib URI or metadata.
46+
for name, expected in (("uri", TLD_URIS[path][index]),
47+
("short-name", TLD_SHORT_NAMES[path][index])):
48+
elements = root.findall("{" + namespace + "}" + name)
49+
if len(elements) != 1 or (elements[0].text or "").strip() != expected:
50+
raise ValueError("{}/src/{}: expected exactly one <{}> with value {!r}".format(
51+
family, path, name, expected))
3752
if translate:
3853
for element in root.iter():
3954
element.tag = element.tag.replace("{" + JAVAX_XML + "}", "{" + JAKARTA_XML + "}")
@@ -43,15 +58,8 @@ def descriptor(text, path, translate):
4358
schema = "{http://www.w3.org/2001/XMLSchema-instance}schemaLocation"
4459
if root.get(schema) == JAVAX_XML + " " + JAVAX_XML + "/web-jsptaglibrary_2_1.xsd":
4560
root.set(schema, JAKARTA_XML + " " + JAKARTA_XML + "/web-jsptaglibrary_3_0.xsd")
46-
uri = root.find("{" + JAKARTA_XML + "}uri")
47-
before, after = TLD_URIS[path]
48-
if uri is not None and uri.text == before:
49-
uri.text = after
50-
# This metadata difference has shipped since Jakarta support was added.
51-
if path == "main/resources/META-INF/java-encoder.tld":
52-
short_name = root.find("{" + JAKARTA_XML + "}short-name")
53-
if short_name is not None and short_name.text == "java-encoder":
54-
short_name.text = "e"
61+
root.find("{" + JAKARTA_XML + "}uri").text = TLD_URIS[path][1]
62+
root.find("{" + JAKARTA_XML + "}short-name").text = TLD_SHORT_NAMES[path][1]
5563

5664
def canonical(element):
5765
return (element.tag, sorted(element.attrib.items()), " ".join((element.text or "").split()),
@@ -97,8 +105,12 @@ def main():
97105
expected = (jsp / path).read_text(encoding="utf-8")
98106
actual = (jakarta / target).read_text(encoding="utf-8")
99107
if path in TLD_URIS:
100-
expected = descriptor(expected, path, True)
101-
actual = descriptor(actual, path, False)
108+
try:
109+
expected = descriptor(expected, path, True)
110+
actual = descriptor(actual, path, False)
111+
except (ValueError, ET.ParseError) as error:
112+
errors.append(str(error))
113+
continue
102114
else:
103115
expected = translated(expected, path)
104116
if expected != actual:

0 commit comments

Comments
 (0)