Skip to content

Commit c1a6a2a

Browse files
Add event stream protocol test trait
1 parent a859ef2 commit c1a6a2a

25 files changed

+2418
-233
lines changed
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
{
2+
"type": "feature",
3+
"description": "Added a new `eventStreamProtocolTests` trait to enable writing shared test suites for event streams just as has been done for standard HTTP protocol requests and responses.",
4+
"pull_requests": [
5+
"[#2803](https://github.com/smithy-lang/smithy/pull/2803)"
6+
]
7+
}

smithy-protocol-test-traits/src/main/java/software/amazon/smithy/protocoltests/traits/ProtocolTestCaseValidator.java

Lines changed: 5 additions & 62 deletions
Original file line numberDiff line numberDiff line change
@@ -4,20 +4,12 @@
44
*/
55
package software.amazon.smithy.protocoltests.traits;
66

7-
import java.io.IOException;
8-
import java.io.StringReader;
97
import java.util.ArrayList;
108
import java.util.Collections;
119
import java.util.List;
1210
import java.util.Optional;
13-
import javax.xml.parsers.DocumentBuilder;
14-
import javax.xml.parsers.DocumentBuilderFactory;
15-
import javax.xml.parsers.ParserConfigurationException;
16-
import org.xml.sax.InputSource;
17-
import org.xml.sax.SAXException;
1811
import software.amazon.smithy.model.Model;
1912
import software.amazon.smithy.model.knowledge.OperationIndex;
20-
import software.amazon.smithy.model.loader.ModelSyntaxException;
2113
import software.amazon.smithy.model.node.Node;
2214
import software.amazon.smithy.model.node.ObjectNode;
2315
import software.amazon.smithy.model.shapes.OperationShape;
@@ -29,7 +21,7 @@
2921
import software.amazon.smithy.model.validation.NodeValidationVisitor;
3022
import software.amazon.smithy.model.validation.ValidationEvent;
3123
import software.amazon.smithy.model.validation.node.TimestampValidationStrategy;
32-
import software.amazon.smithy.utils.MediaType;
24+
import software.amazon.smithy.utils.ListUtils;
3325

3426
/**
3527
* Validates the following:
@@ -48,24 +40,11 @@ abstract class ProtocolTestCaseValidator<T extends Trait> extends AbstractValida
4840
private final Class<T> traitClass;
4941
private final ShapeId traitId;
5042
private final String descriptor;
51-
private final DocumentBuilderFactory documentBuilderFactory;
5243

5344
ProtocolTestCaseValidator(ShapeId traitId, Class<T> traitClass, String descriptor) {
5445
this.traitId = traitId;
5546
this.traitClass = traitClass;
5647
this.descriptor = descriptor;
57-
documentBuilderFactory = DocumentBuilderFactory.newInstance();
58-
59-
// Disallow loading DTDs and more for protocol test contents.
60-
try {
61-
documentBuilderFactory.setFeature("http://apache.org/xml/features/disallow-doctype-decl", true);
62-
documentBuilderFactory.setXIncludeAware(false);
63-
documentBuilderFactory.setExpandEntityReferences(false);
64-
documentBuilderFactory.setFeature("http://xml.org/sax/features/external-parameter-entities", false);
65-
documentBuilderFactory.setFeature("http://xml.org/sax/features/external-general-entities", false);
66-
} catch (ParserConfigurationException e) {
67-
throw new RuntimeException(e);
68-
}
6948
}
7049

7150
@Override
@@ -162,49 +141,13 @@ private NodeValidationVisitor createVisitor(
162141
}
163142

164143
private List<ValidationEvent> validateMediaType(Shape shape, Trait trait, HttpMessageTestCase test) {
165-
// Only validate the body if it's a non-empty string. Some protocols
166-
// require a content-type header even with no payload.
167-
if (!test.getBody().filter(s -> !s.isEmpty()).isPresent()) {
144+
if (!test.getBodyMediaType().isPresent()) {
168145
return Collections.emptyList();
169146
}
170147

171-
String rawMediaType = test.getBodyMediaType().orElse("application/octet-stream");
172-
MediaType mediaType = MediaType.from(rawMediaType);
173-
List<ValidationEvent> events = new ArrayList<>();
174-
if (isXml(mediaType)) {
175-
validateXml(shape, trait, test).ifPresent(events::add);
176-
} else if (isJson(mediaType)) {
177-
validateJson(shape, trait, test).ifPresent(events::add);
178-
}
179-
180-
return events;
181-
}
182-
183-
private boolean isXml(MediaType mediaType) {
184-
return mediaType.getSubtype().equals("xml") || mediaType.getSuffix().orElse("").equals("xml");
185-
}
186-
187-
private boolean isJson(MediaType mediaType) {
188-
return mediaType.getSubtype().equals("json") || mediaType.getSuffix().orElse("").equals("json");
189-
}
190-
191-
private Optional<ValidationEvent> validateXml(Shape shape, Trait trait, HttpMessageTestCase test) {
192-
try {
193-
DocumentBuilder builder = documentBuilderFactory.newDocumentBuilder();
194-
builder.parse(new InputSource(new StringReader(test.getBody().orElse(""))));
195-
return Optional.empty();
196-
} catch (ParserConfigurationException | SAXException | IOException e) {
197-
return Optional.of(emitMediaTypeError(shape, trait, test, e));
198-
}
199-
}
200-
201-
private Optional<ValidationEvent> validateJson(Shape shape, Trait trait, HttpMessageTestCase test) {
202-
try {
203-
Node.parse(test.getBody().orElse(""));
204-
return Optional.empty();
205-
} catch (ModelSyntaxException e) {
206-
return Optional.of(emitMediaTypeError(shape, trait, test, e));
207-
}
148+
return ProtocolTestValidationUtils.validateMediaType(test.getBody().orElse(""), test.getBodyMediaType().get())
149+
.map(e -> ListUtils.of(emitMediaTypeError(shape, trait, test, e)))
150+
.orElse(Collections.emptyList());
208151
}
209152

210153
private ValidationEvent emitMediaTypeError(Shape shape, Trait trait, HttpMessageTestCase test, Throwable e) {
Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
/*
2+
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
3+
* SPDX-License-Identifier: Apache-2.0
4+
*/
5+
package software.amazon.smithy.protocoltests.traits;
6+
7+
import java.io.IOException;
8+
import java.io.StringReader;
9+
import java.util.Optional;
10+
import javax.xml.parsers.DocumentBuilder;
11+
import javax.xml.parsers.DocumentBuilderFactory;
12+
import javax.xml.parsers.ParserConfigurationException;
13+
import org.xml.sax.InputSource;
14+
import org.xml.sax.SAXException;
15+
import software.amazon.smithy.model.loader.ModelSyntaxException;
16+
import software.amazon.smithy.model.node.Node;
17+
import software.amazon.smithy.utils.MediaType;
18+
import software.amazon.smithy.utils.StringUtils;
19+
20+
/**
21+
* Shared validation utility functions for protocol tests.
22+
*/
23+
public final class ProtocolTestValidationUtils {
24+
private static final DocumentBuilderFactory DOCUMENT_BUILDER_FACTORY = DocumentBuilderFactory.newInstance();
25+
26+
static {
27+
// Disallow loading DTDs and more for protocol test contents.
28+
try {
29+
DOCUMENT_BUILDER_FACTORY.setFeature("http://apache.org/xml/features/disallow-doctype-decl", true);
30+
DOCUMENT_BUILDER_FACTORY.setXIncludeAware(false);
31+
DOCUMENT_BUILDER_FACTORY.setExpandEntityReferences(false);
32+
DOCUMENT_BUILDER_FACTORY.setFeature("http://xml.org/sax/features/external-parameter-entities", false);
33+
DOCUMENT_BUILDER_FACTORY.setFeature("http://xml.org/sax/features/external-general-entities", false);
34+
} catch (ParserConfigurationException e) {
35+
throw new RuntimeException(e);
36+
}
37+
}
38+
39+
private ProtocolTestValidationUtils() {}
40+
41+
/**
42+
* Checks whether a body is well-formed according to its mediaType.
43+
*
44+
* <p>If the body is not well-formed, a exception with context will be returned.
45+
*
46+
* <p>Currently XML and JSON validation are supported.
47+
*
48+
* @param body The body to validate.
49+
* @param rawMediaType The mediaType to validate the body with.
50+
* @return Returns an Optional Exception if the body is not valid.
51+
*/
52+
public static Optional<Exception> validateMediaType(String body, String rawMediaType) {
53+
if (StringUtils.isEmpty(body) || StringUtils.isEmpty(rawMediaType)) {
54+
return Optional.empty();
55+
}
56+
57+
MediaType mediaType = MediaType.from(rawMediaType);
58+
if (isXml(mediaType)) {
59+
return validateXml(body);
60+
} else if (isJson(mediaType)) {
61+
return validateJson(body);
62+
}
63+
64+
return Optional.empty();
65+
}
66+
67+
private static boolean isXml(MediaType mediaType) {
68+
return mediaType.getSubtype().equals("xml") || mediaType.getSuffix().orElse("").equals("xml");
69+
}
70+
71+
private static Optional<Exception> validateXml(String body) {
72+
try {
73+
DocumentBuilder builder = DOCUMENT_BUILDER_FACTORY.newDocumentBuilder();
74+
builder.parse(new InputSource(new StringReader(body)));
75+
return Optional.empty();
76+
} catch (ParserConfigurationException | SAXException | IOException e) {
77+
return Optional.of(e);
78+
}
79+
}
80+
81+
private static boolean isJson(MediaType mediaType) {
82+
return mediaType.getSubtype().equals("json") || mediaType.getSuffix().orElse("").equals("json");
83+
}
84+
85+
private static Optional<Exception> validateJson(String body) {
86+
try {
87+
Node.parse(body);
88+
return Optional.empty();
89+
} catch (ModelSyntaxException e) {
90+
return Optional.of(e);
91+
}
92+
}
93+
}
Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,119 @@
1+
/*
2+
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
3+
* SPDX-License-Identifier: Apache-2.0
4+
*/
5+
package software.amazon.smithy.protocoltests.traits;
6+
7+
import java.util.Optional;
8+
import software.amazon.smithy.model.node.ExpectationNotMetException;
9+
import software.amazon.smithy.model.node.Node;
10+
import software.amazon.smithy.model.node.ObjectNode;
11+
import software.amazon.smithy.model.node.ToNode;
12+
import software.amazon.smithy.model.validation.ValidationUtils;
13+
import software.amazon.smithy.utils.ListUtils;
14+
15+
/**
16+
* Defines the expected result of a test case.
17+
*
18+
* <p>This can either be a successful response, any error response, or a
19+
* specific error response.
20+
*/
21+
public final class TestExpectation implements ToNode {
22+
private static final String SUCCESS = "success";
23+
private static final String FAILURE = "failure";
24+
25+
private final TestFailureExpectation failure;
26+
27+
private TestExpectation(TestFailureExpectation failure) {
28+
this.failure = failure;
29+
}
30+
31+
/**
32+
* @return Creates an expectation that the service call for a smoke
33+
* test case is successful.
34+
*/
35+
public static TestExpectation success() {
36+
return new TestExpectation(null);
37+
}
38+
39+
/**
40+
* @param failure The failure to expect.
41+
* @return Creates an expectation that the service call for a smoke test
42+
* case will result in the given failure.
43+
*/
44+
public static TestExpectation failure(TestFailureExpectation failure) {
45+
return new TestExpectation(failure);
46+
}
47+
48+
/**
49+
* Creates a {@link TestExpectation} from a {@link Node}.
50+
*
51+
* @param node Node to deserialize into a {@link TestExpectation}.
52+
* @return Returns the created {@link TestExpectation}.
53+
*/
54+
public static TestExpectation fromNode(Node node) {
55+
ObjectNode o = node.expectObjectNode();
56+
if (o.containsMember(SUCCESS)) {
57+
o.expectNoAdditionalProperties(ListUtils.of(SUCCESS));
58+
return TestExpectation.success();
59+
} else if (o.containsMember(FAILURE)) {
60+
o.expectNoAdditionalProperties(ListUtils.of(FAILURE));
61+
TestFailureExpectation failure = TestFailureExpectation.fromNode(o.expectObjectMember(FAILURE));
62+
return TestExpectation.failure(failure);
63+
} else {
64+
throw new ExpectationNotMetException("Expected an object with exactly one `" + SUCCESS + "` or `" + FAILURE
65+
+ "` property, but found properties: " + ValidationUtils.tickedList(o.getStringMap().keySet()), o);
66+
}
67+
}
68+
69+
/**
70+
* @return Whether the service call is expected to succeed.
71+
*/
72+
public boolean isSuccess() {
73+
return failure == null;
74+
}
75+
76+
/**
77+
* @return Whether the service call is expected to fail.
78+
*/
79+
public boolean isFailure() {
80+
return failure != null;
81+
}
82+
83+
/**
84+
* @return The expected failure, if this expectation is a failure expectation.
85+
*/
86+
public Optional<TestFailureExpectation> getFailure() {
87+
return Optional.ofNullable(failure);
88+
}
89+
90+
@Override
91+
public Node toNode() {
92+
ObjectNode.Builder builder = Node.objectNodeBuilder();
93+
if (this.isSuccess()) {
94+
builder.withMember(SUCCESS, Node.objectNode());
95+
} else {
96+
Node failureNode = this.getFailure()
97+
.map(TestFailureExpectation::toNode)
98+
.orElse(Node.objectNode());
99+
builder.withMember(FAILURE, failureNode);
100+
}
101+
return builder.build();
102+
}
103+
104+
@Override
105+
public boolean equals(Object o) {
106+
if (this == o) {
107+
return true;
108+
} else if (o == null || o.getClass() != getClass()) {
109+
return false;
110+
} else {
111+
return toNode().equals(((TestExpectation) o).toNode());
112+
}
113+
}
114+
115+
@Override
116+
public int hashCode() {
117+
return toNode().hashCode();
118+
}
119+
}

0 commit comments

Comments
 (0)