Issue #56 - Intermediate Commit

This commit is contained in:
Joshua Darnell 2021-04-27 10:34:45 -07:00
parent 094fc33d19
commit dc93fbb9b6
5 changed files with 204 additions and 49 deletions

View File

@ -175,6 +175,37 @@ task testDataDictionary_1_7() {
}
}
task testIdxPayload_1_7() {
group = 'RESO Certification'
description = 'Runs IDX Payload 1.7 Automated Acceptance Tests.' +
'\n Example: ' +
'\n $ ./gradlew testIdxPayload_1_7 -DpathToRESOScript=/path/to/web-api-core-1.0.2.resoscript -DshowResponses=true\n' +
'\n Note: by default the Web API tests assume Collection(Edm.EnumType).' +
'\n Pass -DuseCollections=false if using OData IsFlags.\n'
dependsOn jar
doLast {
javaexec {
main = "io.cucumber.core.cli.Main"
classpath = configurations.cucumberRuntime + sourceSets.main.output + sourceSets.test.output
systemProperties = System.getProperties()
args = [
'--strict',
'--plugin',
'pretty',
'--plugin',
'json:build/idx-payload.dd-1.7.json',
'--plugin',
'html:build/idx-payload.dd-1.7.html',
'--glue',
'org.reso.certification.stepdefs#IDXPayload',
'src/main/java/org/reso/certification/features/payloads/idx-payload.feature'
]
}
}
}
task generateCertificationReport_DD_1_7() {
group = 'RESO Certification'
description = 'Runs Data Dictionary 1.7 tests and creates a certification report' +

View File

@ -16,35 +16,39 @@ Feature: Web API Server Add/Edit Endorsement
# OData-Version: 4.01
# Content-Type: application/json;odata.metadata=minimal
# Accept: application/json
#
# This is without the prefer header and minimal value
#
@create @create-succeeds @add-edit-endorsement @rcp-010 @1.0.2
Scenario: Create operation succeeds using a given payload
Given valid metadata have been retrieved
And request data has been provided in "create-succeeds.json"
And request data in "create-succeeds.json" is valid JSON
And schema in "create-succeeds.json" matches the metadata
And the request header "OData-Version" is "4.01"
And the request header "Content-Type" contains "application/json"
And the request header "Accept" is "application/json"
And the request header "OData-Version" "equals" one of the following values
|4.0|4.01|
And the request header "Content-Type" "contains" "application/json"
And the request header "Accept" "contains" "application/json"
When a "POST" request is made to the "resource-endpoint" URL with data in "create-succeeds.json"
Then the test is skipped if the server responds with a status code of 401
# TODO: check spec for 204
When the server responds with one of the following status codes
|200|201|
Then the response header "OData-Version" is "4.01"
And the response header "EntityId" is present
And the response header "Location" is present
And the response header "Location" is a valid URL
When the server responds with a 200 status code "valid JSON exists" in the JSON response
When the server responds with a 200 status code "@odata.context" "is present" in the JSON response
When the server responds with a 200 status code "@odata.context" "is a valid URL" in the JSON response
When the server responds with a 200 status code "@odata.id" "is present" in the JSON response
When the server responds with a 200 status code "@odata.id" "is a valid URL" in the JSON response
When the server responds with a 200 status code "@odata.editLink" "is present" in the JSON response
When the server responds with a 200 status code "@odata.editLink" "is a valid URL" in the JSON response
When the server responds with a 200 status code "@odata.etag" "is present" in the JSON response
When the server responds with a 200 status code "@odata.etag" "starts with" "W/" in the JSON response
When the server responds with a 200 status code data from "create-succeeds.json" "exists" in the JSON response
When a "GET" request is made to the response header "Location" URL
Then the server responds with one of the following status codes
|201|
And the response header "OData-Version" "equals" one of the following values
|4.0|4.01|
And the response header "EntityId" "MUST" "be present"
And the response header "Location" "MUST" "be present"
And the response header "Location" "is a valid URL"
And the response header "Location" "MUST" reference the resource being created
And the response is valid JSON
And the JSON response "MUST" contain "@odata.context"
And the JSON response value "@odata.context" "is a valid URL"
And the JSON response "MUST" contain "@odata.id"
And the JSON response value "@odata.id" "is a valid URL"
And the JSON response "MAY" contain "@odata.editLink"
And the JSON response value "@odata.editLink" "is a valid URL"
And the JSON response "MAY" contain "@odata.etag"
And the JSON response value "@odata.etag" "starts with" "W/"
And the JSON response "MUST" contain all JSON data in "create-succeeds.json"
When a "GET" request is made to the URL in response header "Location"
Then the server responds with a status code of 200
And the response has header "OData-Version" with one of the following values
|4.0|4.01|
@ -58,7 +62,7 @@ Feature: Web API Server Add/Edit Endorsement
# SEE: https://reso.atlassian.net/wiki/spaces/RESOWebAPIRCP/pages/2239399511/RCP+-+WEBAPI-010+Add+Functionality+to+Web+API+Specification#Error-Message-Example
# POST serviceRoot/Property
# OData-Version: 4.01
# Content-Type: application/json;odata.metadata=minimal
# Content-Type: application/json
# Accept: application/json
@create @create-fails @add-edit-endorsement @rcp-010 @1.0.2
Scenario: Create operation fails using a given payload
@ -66,12 +70,16 @@ Feature: Web API Server Add/Edit Endorsement
And request data has been provided in "create-fails.json"
And request data in "create-fails.json" is valid JSON
And schema in "create-fails.json" matches the metadata
And the request header "OData-Version" is "4.01"
And the request header "Content-Type" is "application/json;odata.metadata=minimal"
And the request header "Accept" is "application/json"
And the request header "OData-Version" "equals" one of the following values
|4.0|4.01|
And the request header "Content-Type" "MUST" "be present"
And the request header "Content-Type" "equals" "application/json"
And the request header "Accept" "MUST" "be present"
And the request header "Accept" "contains" "application/json"
When a "POST" request is made to the "resource-endpoint" URL with data in "create-fails.json"
Then the server responds with one of the following error codes
|400|401|403|405|408|500|501|503|
And the response header "OData-Version" is "4.01"
|400|
And the response has header "OData-Version" with one of the following values
|4.0|4.01|
And the error response is in a valid format
And the values in the "target" field in the JSON payload "error.details" path are contained within the metadata
And the values in the "target" field in the JSON payload "error.details" path are contained within the metadata

View File

@ -12,6 +12,7 @@ import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.apache.olingo.client.api.data.ResWrap;
import org.apache.olingo.commons.api.data.EntityCollection;
import org.apache.olingo.commons.api.edm.EdmKeyPropertyRef;
import org.apache.olingo.commons.api.format.ContentType;
import org.reso.certification.containers.WebAPITestContainer;
import org.reso.commander.Commander;
@ -20,6 +21,7 @@ import org.reso.models.Settings;
import java.io.ByteArrayInputStream;
import java.io.File;
import java.net.URI;
import java.nio.charset.StandardCharsets;
import java.util.*;
import java.util.concurrent.atomic.AtomicBoolean;
@ -27,12 +29,15 @@ import java.util.concurrent.atomic.AtomicReference;
import java.util.stream.Collectors;
import static com.google.common.hash.Hashing.sha256;
import static junit.framework.TestCase.assertNotNull;
import static org.junit.Assume.assumeTrue;
import static org.reso.certification.containers.WebAPITestContainer.EMPTY_STRING;
import static org.reso.commander.common.ErrorMsg.getDefaultErrorMessage;
import static org.reso.commander.common.TestUtils.failAndExitWithErrorMessage;
public class IDXPayload {
private static final Logger LOG = LogManager.getLogger(IDXPayload.class);
private static final CharSequence SEPARATOR_CHARACTER = "|";
private static final CharSequence MODIFICATION_TIMESTAMP_FIELD = "ModificationTimestamp";
private static Scenario scenario;
private final static AtomicBoolean hasStandardResources = new AtomicBoolean(false);
@ -76,7 +81,8 @@ public class IDXPayload {
.map(Object::toString)
.collect(Collectors.toSet());
standardResources.set(resources.stream().filter(DataDictionaryMetadata.v1_7.WELL_KNOWN_RESOURCES::contains).collect(Collectors.toSet()));
standardResources.set(resources.stream()
.filter(DataDictionaryMetadata.v1_7.WELL_KNOWN_RESOURCES::contains).collect(Collectors.toSet()));
nonStandardResources.set(Sets.difference(resources, standardResources.get()));
hasStandardResources.set(standardResources.get().size() > 0);
@ -91,7 +97,7 @@ public class IDXPayload {
}
public static String hashValues(String ...values) {
return sha256().hashString(String.join(EMPTY_STRING, values), StandardCharsets.UTF_8).toString();
return sha256().hashString(String.join(SEPARATOR_CHARACTER, values), StandardCharsets.UTF_8).toString();
}
@When("{int} records are sampled from each RESO Standard resource in the server metadata")
@ -100,44 +106,108 @@ public class IDXPayload {
scenario.log("No RESO Standard Resources to sample");
assumeTrue(true);
} else {
AtomicReference<ResWrap<EntityCollection>> entitySet = new AtomicReference<>();
final AtomicReference<ResWrap<EntityCollection>> entitySet = new AtomicReference<>();
//Map of String Resource Names to Key-Mapped Hash of Field and SHA value
AtomicReference<Map<String, Map<String, List<String>>>> encodedSamples = new AtomicReference<>(new LinkedHashMap<>());
final AtomicReference<Map<String, Map<String, List<Vector<String>>>>> encodedSamples = new AtomicReference<>(new LinkedHashMap<>());
standardResources.get().forEach(resourceName -> {
//create placeholder hashmap for this resource name
encodedSamples.get().put(resourceName, new LinkedHashMap<>());
try {
container.get().setRequestUri(Commander.prepareURI(
container.get().getCommander().getClient().newURIBuilder(
container.get().getServiceRoot()).appendEntitySetSegment(resourceName).build().toString()));
final URI uri = Commander.prepareURI(container.get().getCommander().getClient().newURIBuilder(
container.get().getServiceRoot()).appendEntitySetSegment(resourceName).build().toString());
container.get().setRequestUri(uri);
assertNotNull(getDefaultErrorMessage("Request URI was null!"), uri);
LOG.info("Requesting " + resourceName + " data from: " + uri.toString());
container.get().executePreparedRawGetRequest();
entitySet.set(container.get().getCommander().getClient().getDeserializer(ContentType.APPLICATION_JSON).toEntitySet(new ByteArrayInputStream(container.get().getResponseData().getBytes())));
entitySet.set(container.get().getCommander().getClient()
.getDeserializer(ContentType.APPLICATION_JSON)
.toEntitySet(new ByteArrayInputStream(container.get().getResponseData().getBytes())));
} catch (Exception exception) {
LOG.error(exception);
} finally {
final String keyField = container.get().getEdm().getEntityContainer().getEntitySet(resourceName).getEntityType().getKeyPropertyRefs().get(0).getName();
//assumes the 0th key is always used. TODO: determine if we need to scan the keys.
final List<EdmKeyPropertyRef> keyFields = container.get().getEdm().getEntityContainer()
.getEntitySet(resourceName).getEntityType().getKeyPropertyRefs();
LOG.debug("Keys is: " + keyFields.stream().map(EdmKeyPropertyRef::getName).collect(Collectors.joining(", ")));
assert(keyFields.size() > 0) :
getDefaultErrorMessage("no Key Fields found! Resources MUST have at least one key.");
//we will always key the hash by the first key, the other key fields will still be there
//for the MUST requirement checks
final String keyField = keyFields.get(0).getName();
LOG.info("Hashing " + resourceName + " payload values...");
entitySet.get().getPayload().getEntities().forEach(entity -> {
entity.getProperties().forEach(property -> {
entity.getProperties().parallelStream().forEach(property -> {
if (!encodedSamples.get().get(resourceName).containsKey(keyField)) {
encodedSamples.get().get(resourceName).put(keyField, new LinkedList<>());
encodedSamples.get().get(resourceName).put(entity.getProperty(keyField).getValue().toString(), new LinkedList<>());
}
encodedSamples.get().get(resourceName).get(keyField).add(property.getName());
encodedSamples.get().get(resourceName).get(keyField).add(
property.getName().contentEquals(keyField)
? property.getValue().toString() : (property.getValue() != null
? hashValues(property.getValue().toString()) : null));
final Vector<String> fieldMeta = new Vector<>();
fieldMeta.setSize(3);
fieldMeta.set(0, property.getName());
fieldMeta.set(1, property.getName().contentEquals(MODIFICATION_TIMESTAMP_FIELD) || keyFields.stream().reduce(false,
(acc, f) -> acc && f.getName().contentEquals(property.getName()), Boolean::logicalAnd)
? property.getValue().toString() : (property.getValue() != null
? hashValues(property.getValue().toString()) : null));
fieldMeta.set(2, property.getValue() == null ? null :
(keyFields.stream().anyMatch(f -> f.getName().contentEquals(property.getName())) ? "primaryKey" : null));
LOG.info("Adding fieldMeta: " + fieldMeta.toString());
encodedSamples.get().get(resourceName).get(entity.getProperty(keyField).getValue().toString()).add(fieldMeta);
});
});
LOG.info(encodedSamples.get().toString());
LOG.info("Values encoded!");
//createDataAvailabilityReport(encodedSamples);
}
LOG.info("Records Sampled: " + encodedSamples.get().get(resourceName).values().size());
});
}
}
public void createDataAvailabilityReport(AtomicReference<Map<String, Map<String, List<Vector<String>>>>> encodedSamples) {
AtomicReference<Map<String, Map<String, Integer>>> resourceTallies = new AtomicReference<>(new LinkedHashMap<>());
encodedSamples.get().keySet().forEach(resourceName -> {
LOG.debug("Processing resource: " + resourceName);
LOG.debug("Sample size: " + encodedSamples.get().get(resourceName).keySet().size());
//for each resource, go through the keys and tally the data presence counts for each field
//as well as the number of samples in each case
resourceTallies.get().put(resourceName, new LinkedHashMap<>());
encodedSamples.get().get(resourceName).forEach((key, value) -> {
if (value != null) {
value.forEach(sample -> {
if (sample != null) {
//if element has a value
if (sample.get(1) != null) {
//if the value hasn't already been tallied, then just add one
if (!resourceTallies.get().get(resourceName).containsKey(sample.get(0))) {
resourceTallies.get().get(resourceName).put(sample.get(0), 1);
} else {
//otherwise, increment the current value
resourceTallies.get().get(resourceName).put(sample.get(0),
resourceTallies.get().get(resourceName).get(sample.get(0) + 1));
}
}
}
});
}
});
});
LOG.info(resourceTallies.get());
}
@When("{int} records are sampled from each non standard resource in the server metadata")
public void recordsAreSampledFromEachNonStandardResourceInTheServerMetadata(int numRecords) {
}

View File

@ -95,4 +95,48 @@ public class WebAPIServerAddEdit {
public void theRequestHeaderContains(String arg0, String arg1) {
}
@And("the request header {string} {string} one of the following values")
public void theRequestHeaderOneOfTheFollowingValues(String arg0, String arg1) {
}
@And("the request header {string} {string} {string}")
public void theRequestHeader(String arg0, String arg1, String arg2) {
}
@And("the response header {string} {string} one of the following values")
public void theResponseHeaderOneOfTheFollowingValues(String arg0, String arg1) {
}
@And("the response header {string} {string} {string}")
public void theResponseHeader(String arg0, String arg1, String arg2) {
}
@And("the response header {string} {string}")
public void theResponseHeader(String arg0, String arg1) {
}
@And("the response header {string} {string} reference the resource being created")
public void theResponseHeaderReferenceTheResourceBeingCreated(String arg0, String arg1) {
}
@And("the JSON response {string} contain {string}")
public void theJSONResponseContain(String arg0, String arg1) {
}
@And("the JSON response value {string} {string}")
public void theJSONResponseValue(String arg0, String arg1) {
}
@And("the JSON response value {string} {string} {string}")
public void theJSONResponseValue(String arg0, String arg1, String arg2) {
}
@And("the JSON response {string} contain all JSON data in {string}")
public void theJSONResponseContainAllJSONDataIn(String arg0, String arg1) {
}
@When("a {string} request is made to the URL in response header {string}")
public void aRequestIsMadeToTheURLInResponseHeader(String arg0, String arg1) {
}
}

View File

@ -77,7 +77,6 @@
}
},
"required": [
"annotations",
"fieldName",
"isCollection",
"nullable",
@ -89,8 +88,11 @@
},
"Annotation": {
"type": "object",
"additionalProperties": false,
"additionalProperties": true,
"properties": {
"term": {
"type": "string"
},
"value": {
"type": "string",
"qt-uri-protocols": [