Skip to content

Commit 0678613

Browse files
committed
Validate known media types in protocol tests
This commit updates protocol test validators to ensure that known media types for XML and JSON contain valid content. Closes #821
1 parent a368236 commit 0678613

File tree

10 files changed

+241
-11
lines changed

10 files changed

+241
-11
lines changed

smithy-aws-protocol-tests/model/ec2Query/xml-structs.smithy

+2-2
Original file line numberDiff line numberDiff line change
@@ -394,7 +394,7 @@ apply XmlNamespaces @httpResponseTests([
394394
protocol: ec2Query,
395395
code: 200,
396396
body: """
397-
<XmlNamespacesResponse xmlns="http://foo.com" xmlns="https://example.com/">
397+
<XmlNamespacesResponse xmlns="https://example.com/">
398398
<nested>
399399
<foo xmlns:baz="http://baz.com">Foo</foo>
400400
<values xmlns="http://qux.com">
@@ -457,7 +457,7 @@ apply IgnoresWrappingXmlName @httpResponseTests([
457457
protocol: ec2Query,
458458
code: 200,
459459
body: """
460-
<IgnoresWrappingXmlNameResponse xmlns="http://foo.com" xmlns="https://example.com/">
460+
<IgnoresWrappingXmlNameResponse xmlns="https://example.com/">
461461
<foo>bar</foo>
462462
<RequestId>requestid</RequestId>
463463
</IgnoresWrappingXmlNameResponse>

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

+89-9
Original file line numberDiff line numberDiff line change
@@ -15,13 +15,20 @@
1515

1616
package software.amazon.smithy.protocoltests.traits;
1717

18+
import java.io.IOException;
19+
import java.io.StringReader;
1820
import java.util.ArrayList;
21+
import java.util.Collections;
1922
import java.util.List;
2023
import java.util.Optional;
21-
import java.util.stream.Collectors;
22-
import java.util.stream.Stream;
24+
import javax.xml.parsers.DocumentBuilder;
25+
import javax.xml.parsers.DocumentBuilderFactory;
26+
import javax.xml.parsers.ParserConfigurationException;
27+
import org.xml.sax.InputSource;
28+
import org.xml.sax.SAXException;
2329
import software.amazon.smithy.model.Model;
2430
import software.amazon.smithy.model.knowledge.OperationIndex;
31+
import software.amazon.smithy.model.loader.ModelSyntaxException;
2532
import software.amazon.smithy.model.node.Node;
2633
import software.amazon.smithy.model.node.ObjectNode;
2734
import software.amazon.smithy.model.shapes.OperationShape;
@@ -33,29 +40,44 @@
3340
import software.amazon.smithy.model.validation.NodeValidationVisitor;
3441
import software.amazon.smithy.model.validation.ValidationEvent;
3542
import software.amazon.smithy.model.validation.node.TimestampValidationStrategy;
43+
import software.amazon.smithy.utils.MediaType;
3644

45+
/**
46+
* Validates the following:
47+
*
48+
* <ul>
49+
* <li>XML and JSON bodyMediaTypes contain valid content.</li>
50+
* <li>vendorParamsShape is a valid shape.</li>
51+
* <li>Vendor params are compatible with any referenced shape.</li>
52+
* <li>Params for a test case are valid for the model.</li>
53+
* </ul>
54+
*
55+
* @param <T> Type of test case to validate.
56+
*/
3757
abstract class ProtocolTestCaseValidator<T extends Trait> extends AbstractValidator {
3858

3959
private final Class<T> traitClass;
4060
private final ShapeId traitId;
4161
private final String descriptor;
62+
private final DocumentBuilderFactory documentBuilderFactory;
4263

4364
ProtocolTestCaseValidator(ShapeId traitId, Class<T> traitClass, String descriptor) {
4465
this.traitId = traitId;
4566
this.traitClass = traitClass;
4667
this.descriptor = descriptor;
68+
documentBuilderFactory = DocumentBuilderFactory.newInstance();
4769
}
4870

4971
@Override
5072
public List<ValidationEvent> validate(Model model) {
5173
OperationIndex operationIndex = OperationIndex.of(model);
5274

53-
return Stream.concat(model.shapes(OperationShape.class), model.shapes(StructureShape.class))
54-
.filter(shape -> shape.hasTrait(traitClass))
55-
.flatMap(shape -> {
56-
return validateOperation(model, operationIndex, shape, shape.expectTrait(traitClass)).stream();
57-
})
58-
.collect(Collectors.toList());
75+
List<ValidationEvent> events = new ArrayList<>();
76+
for (Shape shape : model.getShapesWithTrait(traitClass)) {
77+
events.addAll(validateShape(model, operationIndex, shape, shape.expectTrait(traitClass)));
78+
}
79+
80+
return events;
5981
}
6082

6183
abstract StructureShape getStructure(Shape shape, OperationIndex operationIndex);
@@ -66,7 +88,7 @@ boolean isValidatedBy(Shape shape) {
6688
return shape instanceof OperationShape;
6789
}
6890

69-
private List<ValidationEvent> validateOperation(
91+
private List<ValidationEvent> validateShape(
7092
Model model,
7193
OperationIndex operationIndex,
7294
Shape shape,
@@ -78,6 +100,9 @@ private List<ValidationEvent> validateOperation(
78100
for (int i = 0; i < testCases.size(); i++) {
79101
HttpMessageTestCase testCase = testCases.get(i);
80102

103+
// Validate the syntax of known media types like XML and JSON.
104+
events.addAll(validateMediaType(shape, trait, testCase));
105+
81106
// Validate the vendorParams for the test case if we have a shape defined.
82107
Optional<ShapeId> vendorParamsShapeOptional = testCase.getVendorParamsShape();
83108
ObjectNode vendorParams = testCase.getVendorParams();
@@ -127,4 +152,59 @@ private NodeValidationVisitor createVisitor(
127152
.allowBoxedNull(true)
128153
.build();
129154
}
155+
156+
private List<ValidationEvent> validateMediaType(Shape shape, Trait trait, HttpMessageTestCase test) {
157+
// Only validate the body if it's a non-empty string. Some protocols
158+
// require a content-type header even with no payload.
159+
if (!test.getBody().filter(s -> !s.isEmpty()).isPresent()) {
160+
return Collections.emptyList();
161+
}
162+
163+
String rawMediaType = test.getBodyMediaType().orElse("application/octet-stream");
164+
MediaType mediaType = MediaType.from(rawMediaType);
165+
List<ValidationEvent> events = new ArrayList<>();
166+
if (isXml(mediaType)) {
167+
validateXml(shape, trait, test).ifPresent(events::add);
168+
} else if (isJson(mediaType)) {
169+
validateJson(shape, trait, test).ifPresent(events::add);
170+
}
171+
172+
return events;
173+
}
174+
175+
private boolean isXml(MediaType mediaType) {
176+
return mediaType.getSubtype().equals("xml") || mediaType.getSuffix().orElse("").equals("xml");
177+
}
178+
179+
private boolean isJson(MediaType mediaType) {
180+
return mediaType.getSubtype().equals("json") || mediaType.getSuffix().orElse("").equals("json");
181+
}
182+
183+
private Optional<ValidationEvent> validateXml(Shape shape, Trait trait, HttpMessageTestCase test) {
184+
try {
185+
DocumentBuilder builder = documentBuilderFactory.newDocumentBuilder();
186+
builder.parse(new InputSource(new StringReader(test.getBody().orElse(""))));
187+
return Optional.empty();
188+
} catch (ParserConfigurationException | SAXException | IOException e) {
189+
return Optional.of(emitMediaTypeError(shape, trait, test, e));
190+
}
191+
}
192+
193+
private Optional<ValidationEvent> validateJson(Shape shape, Trait trait, HttpMessageTestCase test) {
194+
try {
195+
Node.parse(test.getBody().orElse(""));
196+
return Optional.empty();
197+
} catch (ModelSyntaxException e) {
198+
return Optional.of(emitMediaTypeError(shape, trait, test, e));
199+
}
200+
}
201+
202+
private ValidationEvent emitMediaTypeError(Shape shape, Trait trait, HttpMessageTestCase test, Throwable e) {
203+
return danger(shape, trait, String.format(
204+
"Invalid %s content in `%s` protocol test case `%s`: %s",
205+
test.getBodyMediaType().orElse(""),
206+
trait.toShapeId(),
207+
test.getId(),
208+
e.getMessage()));
209+
}
130210
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
[DANGER] smithy.example#SayHello: Invalid application/json content in `smithy.test#httpRequestTests` protocol test case `foo1` | HttpRequestTestsInput
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
namespace smithy.example
2+
3+
use smithy.test#httpRequestTests
4+
5+
@trait
6+
@protocolDefinition
7+
structure testProtocol {}
8+
9+
@http(method: "POST", uri: "/")
10+
@httpRequestTests([
11+
{
12+
id: "foo1",
13+
protocol: testProtocol,
14+
method: "POST",
15+
uri: "/",
16+
params: {
17+
type: true
18+
},
19+
bodyMediaType: "application/json",
20+
body: """
21+
{
22+
"foo": "Oh no, we are missing a comma!"
23+
"bar": true
24+
}
25+
"""
26+
}
27+
])
28+
operation SayHello {
29+
input: SayHelloInput
30+
}
31+
32+
structure SayHelloInput {
33+
type: Boolean
34+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
[DANGER] smithy.example#SayHello: Invalid application/xml content in `smithy.test#httpRequestTests` protocol test case `foo1` | HttpRequestTestsInput
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
namespace smithy.example
2+
3+
use smithy.test#httpRequestTests
4+
5+
@trait
6+
@protocolDefinition
7+
structure testProtocol {}
8+
9+
@http(method: "POST", uri: "/")
10+
@httpRequestTests([
11+
{
12+
id: "foo1",
13+
protocol: testProtocol,
14+
method: "POST",
15+
uri: "/",
16+
params: {
17+
type: true
18+
},
19+
bodyMediaType: "application/xml",
20+
body: """
21+
<XmlNamespacesResponse xmlns="http://foo.com" xmlns="https://example.com/">
22+
<nested>
23+
<foo xmlns:baz="http://baz.com">Foo</foo>
24+
<values xmlns="http://qux.com">
25+
<member xmlns="http://bux.com">Bar</member>
26+
<member xmlns="http://bux.com">Baz</member>
27+
</values>
28+
</nested>
29+
<RequestId>requestid</RequestId>
30+
</XmlNamespacesResponse>
31+
"""
32+
}
33+
])
34+
operation SayHello {
35+
input: SayHelloInput
36+
}
37+
38+
structure SayHelloInput {
39+
type: Boolean
40+
}

smithy-protocol-test-traits/src/test/resources/software/amazon/smithy/protocoltests/traits/errorfiles/valid-json.errors

Whitespace-only changes.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
namespace smithy.example
2+
3+
use smithy.test#httpRequestTests
4+
5+
@trait
6+
@protocolDefinition
7+
structure testProtocol {}
8+
9+
@http(method: "POST", uri: "/")
10+
@httpRequestTests([
11+
{
12+
id: "foo1",
13+
protocol: testProtocol,
14+
method: "POST",
15+
uri: "/",
16+
params: {
17+
type: true
18+
},
19+
bodyMediaType: "application/json",
20+
body: """
21+
{
22+
"foo": true,
23+
"bar": true
24+
}
25+
"""
26+
}
27+
])
28+
operation SayHello {
29+
input: SayHelloInput
30+
}
31+
32+
structure SayHelloInput {
33+
type: Boolean
34+
}

smithy-protocol-test-traits/src/test/resources/software/amazon/smithy/protocoltests/traits/errorfiles/valid-xml.errors

Whitespace-only changes.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
namespace smithy.example
2+
3+
use smithy.test#httpRequestTests
4+
5+
@trait
6+
@protocolDefinition
7+
structure testProtocol {}
8+
9+
@http(method: "POST", uri: "/")
10+
@httpRequestTests([
11+
{
12+
id: "foo1",
13+
protocol: testProtocol,
14+
method: "POST",
15+
uri: "/",
16+
params: {
17+
type: true
18+
},
19+
bodyMediaType: "application/xml",
20+
body: """
21+
<XmlNamespacesResponse xmlns="https://example.com/">
22+
<nested>
23+
<foo xmlns:baz="http://baz.com">Foo</foo>
24+
<values xmlns="http://qux.com">
25+
<member xmlns="http://bux.com">Bar</member>
26+
<member xmlns="http://bux.com">Baz</member>
27+
</values>
28+
</nested>
29+
<RequestId>requestid</RequestId>
30+
</XmlNamespacesResponse>
31+
"""
32+
}
33+
])
34+
operation SayHello {
35+
input: SayHelloInput
36+
}
37+
38+
structure SayHelloInput {
39+
type: Boolean
40+
}

0 commit comments

Comments
 (0)