Skip to content

Expand protocol test coverage for json1.0, query and cbor #6245

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 15 commits into from
Jul 10, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -18,9 +18,16 @@
public class XmlNamespace {

private String prefix;

private String uri;

public XmlNamespace() {

}

public XmlNamespace(String uri) {
this.uri = uri;
}

public String getPrefix() {
return prefix;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -302,6 +302,11 @@ public String getEnumValueName(String enumValue) {
// Special cases
result = result.replace("textORcsv", "TEXT_OR_CSV");

// leading digits, add a prefix
if (result.matches("^\\d.*")) {
result = "VALUE_" + result;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I understand the java limitation of not being able to have a number as an enum value, but can you explain how this substitution works in practice?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When the value has leading digits (eg: 0 or 123_some_value) the regex will match, and then we just prefix it with "VALUE", so we end up with VALUE_0 and VALUE_123_SOME_VALUE for those cases.

}

// Split into words
result = String.join("_", splitOnWordBoundaries(result));

Expand Down
4 changes: 4 additions & 0 deletions test/protocol-tests-core/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -137,6 +137,10 @@
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-annotations</artifactId>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.dataformat</groupId>
<artifactId>jackson-dataformat-cbor</artifactId>
</dependency>
</dependencies>

<!-- Disable spotbugs for this test module to speed up the build. -->
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
/*
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License").
* You may not use this file except in compliance with the License.
* A copy of the License is located at
*
* http://aws.amazon.com/apache2.0
*
* or in the "license" file accompanying this file. This file 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 software.amazon.awssdk.protocol.asserts.marshalling;

import static org.junit.Assert.assertEquals;

import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.node.ArrayNode;
import com.fasterxml.jackson.databind.node.DoubleNode;
import com.fasterxml.jackson.databind.node.ObjectNode;
import com.fasterxml.jackson.dataformat.cbor.CBORFactory;
import com.github.tomakehurst.wiremock.verification.LoggedRequest;
import java.util.Base64;

/**
* Asserts on the body (expected to be CBOR) of the marshalled request.
*/
public class CborBodyAssertion extends MarshallingAssertion {
private static final ObjectMapper MAPPER = new ObjectMapper(new CBORFactory());

private final String cborEquals;

public CborBodyAssertion(String cborEquals) {
this.cborEquals = cborEquals;
}

@Override
protected void doAssert(LoggedRequest actual) throws Exception {
JsonNode expected = normalizeToDoubles(MAPPER.readTree(Base64.getDecoder().decode(cborEquals)));
JsonNode actualJson = normalizeToDoubles(MAPPER.readTree(actual.getBody()));
assertEquals(expected, actualJson);
}

/**
* CBOR allows serializing floating point numbers to different sizes (eg, float16/32/64).
* However, in assertEquals float and double nodes will never be equal so we convert them
* all to doubles.
*/
private JsonNode normalizeToDoubles(JsonNode node) {
if (node.isFloat() || node.isDouble()) {
return DoubleNode.valueOf(node.doubleValue());
}
if (node.isArray()) {
ArrayNode array = MAPPER.createArrayNode();
for (JsonNode item : node) {
array.add(normalizeToDoubles(item));
}
return array;
}
if (node.isObject()) {
ObjectNode obj = MAPPER.createObjectNode();
node.fields().forEachRemaining(entry -> obj.set(entry.getKey(), normalizeToDoubles(entry.getValue())));
return obj;
}
return node;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@

import com.github.tomakehurst.wiremock.http.HttpHeaders;
import com.github.tomakehurst.wiremock.verification.LoggedRequest;
import java.util.Collections;
import java.util.List;
import java.util.Map;

Expand All @@ -32,6 +33,7 @@ public class HeadersAssertion extends MarshallingAssertion {
private Map<String, List<String>> contains;

private List<String> doesNotContain;
private List<String> mustContain;

public void setContains(Map<String, List<String>> contains) {
this.contains = contains;
Expand All @@ -41,6 +43,10 @@ public void setDoesNotContain(List<String> doesNotContain) {
this.doesNotContain = doesNotContain;
}

public void setMustContain(List<String> mustContain) {
this.mustContain = mustContain;
}

@Override
protected void doAssert(LoggedRequest actual) throws Exception {
if (contains != null) {
Expand All @@ -49,13 +55,20 @@ protected void doAssert(LoggedRequest actual) throws Exception {
if (doesNotContain != null) {
assertDoesNotContainHeaders(actual.getHeaders());
}
if (mustContain != null) {
assertMustContainHeaders(actual.getHeaders());
}
}

private void assertHeadersContains(HttpHeaders actual) {
contains.forEach((expectedKey, expectedValues) -> {
assertTrue(String.format("Header '%s' was expected to be present. Actual headers: %s", expectedKey, actual),
actual.getHeader(expectedKey).isPresent());
List<String> actualValues = actual.getHeader(expectedKey).values();
// the Java SDK adds charset to content-type. This is valid but not included in the protocol tests
if (expectedKey.equalsIgnoreCase("Content-Type") && actualValues.size() == 1) {
actualValues = Collections.singletonList(actualValues.get(0).replace("; charset=UTF-8", ""));
}
assertEquals(expectedValues, actualValues);
});
}
Expand All @@ -66,4 +79,11 @@ private void assertDoesNotContainHeaders(HttpHeaders actual) {
actual.getHeader(headerName).isPresent());
});
}

private void assertMustContainHeaders(HttpHeaders actual) {
mustContain.forEach(headerName -> {
assertTrue(String.format("Header '%s' was expected to be present", headerName),
actual.getHeader(headerName).isPresent());
});
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,10 @@

import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.node.ArrayNode;
import com.fasterxml.jackson.databind.node.IntNode;
import com.fasterxml.jackson.databind.node.LongNode;
import com.fasterxml.jackson.databind.node.ObjectNode;
import com.github.tomakehurst.wiremock.verification.LoggedRequest;

/**
Expand All @@ -37,8 +41,39 @@ public JsonBodyAssertion(String jsonEquals) {
@Override
protected void doAssert(LoggedRequest actual) throws Exception {
JsonNode expected = MAPPER.readTree(jsonEquals);
JsonNode actualJson = MAPPER.readTree(actual.getBodyAsString());
JsonNode actualJson = convertWholeNumberDoubleToLong(MAPPER.readTree(actual.getBodyAsString()));
assertEquals(expected, actualJson);
}

/**
* We serialize some numbers (in particular epoch timestamps) as doubles such as 123.000.
* In protocol tests, these values are parsed as longs. This conversion insures that
* 123.000 will equal 123.
*/
public static JsonNode convertWholeNumberDoubleToLong(JsonNode node) {
if (node.isDouble()) {
double value = node.doubleValue();
if (value % 1 == 0) {
if (value >= Integer.MIN_VALUE && value <= Integer.MAX_VALUE) {
return new IntNode((int) value);
}
return new LongNode((long) value);
}
}
if (node.isObject()) {
ObjectNode obj = (ObjectNode) node;
ObjectNode result = obj.objectNode();
obj.fieldNames().forEachRemaining(field -> result.set(field, convertWholeNumberDoubleToLong(obj.get(field))));
return result;
}
if (node.isArray()) {
ArrayNode array = (ArrayNode) node;
ArrayNode result = array.arrayNode();
for (JsonNode item : array) {
result.add(convertWholeNumberDoubleToLong(item));
}
return result;
}
return node;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
/*
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License").
* You may not use this file except in compliance with the License.
* A copy of the License is located at
*
* http://aws.amazon.com/apache2.0
*
* or in the "license" file accompanying this file. This file 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 software.amazon.awssdk.protocol.asserts.marshalling;

import static org.hamcrest.Matchers.containsInAnyOrder;
import static org.hamcrest.Matchers.equalTo;
import static org.junit.Assert.assertThat;
import static software.amazon.awssdk.protocol.asserts.marshalling.QueryUtils.parseQueryParams;
import static software.amazon.awssdk.protocol.asserts.marshalling.QueryUtils.parseQueryParamsFromBody;

import com.github.tomakehurst.wiremock.verification.LoggedRequest;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;

public class QueryBodyAssertion extends MarshallingAssertion {
private final String queryEquals;

public QueryBodyAssertion(String queryEquals) {
this.queryEquals = queryEquals;
}

@Override
protected void doAssert(LoggedRequest actual) throws Exception {
Map<String, List<String>> expectedParams = parseQueryParamsFromBody(queryEquals);
try {
Map<String, List<String>> actualParams = parseQueryParams(actual);
doAssert(expectedParams, actualParams);
} catch (AssertionError error) {
// We may send the query params in the body if there is no other content. Try
// decoding body as params and rerun the assertions.
Map<String, List<String>> actualParams = parseQueryParamsFromBody(
actual.getBodyAsString());
doAssert(expectedParams, actualParams);
}
}

private void doAssert(Map<String, List<String>> expectedParams, Map<String, List<String>> actualParams) {
assertThat(actualParams.keySet(), equalTo(expectedParams.keySet()));
expectedParams.forEach((key, value) -> assertParamsEqual(actualParams.get(key), value));
}

private void assertParamsEqual(List<String> actual, List<String> expected) {
if (expected.stream().allMatch(QueryBodyAssertion::isNumeric)) {
assertThat(
actual.stream().map(Double::parseDouble).collect(Collectors.toList()),
containsInAnyOrder(expected.stream().map(Double::parseDouble).toArray()));
} else {
assertThat(actual, containsInAnyOrder(expected.toArray()));
}
}

public static boolean isNumeric(String str) {
if (str == null || str.isEmpty()) {
return false;
}
try {
Double.parseDouble(str);
return true;
} catch (NumberFormatException e) {
return false;
}
}


}
Loading