diff --git a/src/main/java/com/dougnoel/sentinel/apis/API.java b/src/main/java/com/dougnoel/sentinel/apis/API.java index eadf16c6..7a3b89d6 100644 --- a/src/main/java/com/dougnoel/sentinel/apis/API.java +++ b/src/main/java/com/dougnoel/sentinel/apis/API.java @@ -1,7 +1,9 @@ package com.dougnoel.sentinel.apis; import java.net.URISyntaxException; +import java.util.HashMap; import java.util.List; +import java.util.Map; import org.apache.http.client.utils.URIBuilder; import com.dougnoel.sentinel.configurations.Configuration; @@ -9,6 +11,7 @@ import com.dougnoel.sentinel.exceptions.IOException; import com.dougnoel.sentinel.system.YAMLObject; +import io.cucumber.java.Scenario; import io.swagger.parser.OpenAPIParser; import io.swagger.v3.oas.models.OpenAPI; import io.swagger.v3.oas.models.servers.Server; @@ -16,6 +19,7 @@ public class API extends YAMLObject { private Request request = new Request(); + protected Map responses = new HashMap<>(); /** * Constructor @@ -44,8 +48,33 @@ protected URIBuilder getURIBuilder(String passedText) { } } + /** + * Returns the currently constructed request for this API. + * + * @return com.dougnoel.sentinel.apis.Request the currently constructed request. + */ public Request getRequest() { return request; } + + /** + * Stores a Response based on the key passed. + * + * @param key String the key to store the Response under + * @param com.dougnoel.sentinel.apis.Response the Response object to store + */ + public void setResponse(String key, Response response) { + responses.put(key, response); + } + + /** + * Returns the stored response for the given key. + * + * @param key String the key the Response is stored under + * @return com.dougnoel.sentinel.apis.Response the Response object requested + */ + public Response getResponse(String key) { + return responses.get(key); + } } diff --git a/src/main/java/com/dougnoel/sentinel/apis/APIManager.java b/src/main/java/com/dougnoel/sentinel/apis/APIManager.java index a83dfdde..c5ec4cfd 100644 --- a/src/main/java/com/dougnoel/sentinel/apis/APIManager.java +++ b/src/main/java/com/dougnoel/sentinel/apis/APIManager.java @@ -5,6 +5,8 @@ import com.dougnoel.sentinel.enums.RequestType; import com.dougnoel.sentinel.system.TestManager; +import io.cucumber.java.Scenario; + /** * Tracks which API is currently being used and requests the APIFactory create it if it does not exist. * @author dougnoel@gmail.com @@ -14,6 +16,7 @@ public class APIManager { //Only one API should be in use at a time. We are consciously not multi-threading. private static API api = null; private static Response response = null; + private static Scenario scenario = null; private APIManager() { // Exists only to defeat instantiation. @@ -75,23 +78,42 @@ public static void addHeader(String name, String value) { * @param endpoint the endpoint to send the request */ public static void sendRequest(RequestType type, String endpoint) { - getAPI().getRequest().createAndSendRequest(type, endpoint); + response = getAPI().getRequest().createAndSendRequest(type, endpoint); + getAPI().setResponse(endpoint, response); } /** * Returns the most recent response. - * @return Response the response + * @return com.dougnoel.sentinel.apis.Response the Response object requested */ public static Response getResponse() { return response; } + + /** + * Returns the response stored for the given endpoint under the currently active API. + * + * @param endpoint String the name of the endpoint the request was sent to + * @return com.dougnoel.sentinel.apis.Response the Response object requested + */ + public static Response getResponse(String endpoint) { + return getAPI().getResponse(endpoint); + } /** - * Sets the most recent response. - * @param response Response the response + * @return Scenario the scenario */ - public static void setResponse(Response response) { - APIManager.response = response; + public static Scenario getScenario() { + return scenario; } + + /** + * @param scenario Scenario the scenario to set + */ + public static void setScenario(Scenario scenario) { + APIManager.scenario = scenario; + } + + } \ No newline at end of file diff --git a/src/main/java/com/dougnoel/sentinel/apis/Request.java b/src/main/java/com/dougnoel/sentinel/apis/Request.java index 2cf73674..6a04f2fe 100644 --- a/src/main/java/com/dougnoel/sentinel/apis/Request.java +++ b/src/main/java/com/dougnoel/sentinel/apis/Request.java @@ -95,13 +95,14 @@ public void setBody(String body) { } /** - * Construct a request, send it to the active API, and store the response for retrieval. + * Construct a request, send it to the active API, and return the response. * Parameterization is handled at the cucumber step level. * * @param type com.dougnoel.sentinel.enums.RequestType the type of request to send * @param endpoint the endpoint to send the request + * @return com.dougnoel.sentinel.apis.Response the response encapsulated in a Response object */ - public void createAndSendRequest(RequestType type, String endpoint) { + public Response createAndSendRequest(RequestType type, String endpoint) { try { switch(type) { case DELETE: @@ -124,14 +125,16 @@ public void createAndSendRequest(RequestType type, String endpoint) { } setHeaders(); buildURI(); - sendRequest(); + return sendRequest(); } /** - * Send the request, store the response for later retrieval, and reset the request so it can be used again - * by the API for another request. + * Returns the response from the server after passing the request to the API server. + * Clears out the request so that the request object can be used again. + * + * @return com.dougnoel.sentinel.apis.Response the response encapsulated in a Response object */ - private void sendRequest() { + private Response sendRequest() { HttpClient httpClient = HttpClientBuilder.create().build(); Response response; try { @@ -141,9 +144,8 @@ private void sendRequest() { } catch (java.io.IOException e) { throw new IOException(e); } - log.trace("Response Code: {} Response: {}", response.getResponseCode(), response.getResponse()); - APIManager.setResponse(response); reset(); + return response; } /** diff --git a/src/main/java/com/dougnoel/sentinel/apis/Response.java b/src/main/java/com/dougnoel/sentinel/apis/Response.java index 86a74528..e27954e3 100644 --- a/src/main/java/com/dougnoel/sentinel/apis/Response.java +++ b/src/main/java/com/dougnoel/sentinel/apis/Response.java @@ -7,7 +7,7 @@ import com.dougnoel.sentinel.strings.SentinelStringUtils; /** - * Wrapper for an hhtp response for testing the response. + * Wrapper for an http response for testing the response. * @author dougnoel * */ @@ -19,7 +19,7 @@ public class Response { /** * - * @param httpResponse HttpResponse the aPI call response used to create this object + * @param httpResponse HttpResponse the API call response used to create this object * @throws IOException if the parsing fails */ public Response(HttpResponse httpResponse) throws IOException { @@ -29,8 +29,11 @@ public Response(HttpResponse httpResponse) throws IOException { /** * Returns the http response as a String + * @deprecated + * This method is no longer needed for comparison operations. * @return String the http response */ + @Deprecated public String getResponse() { return jsonResponse; } @@ -58,4 +61,48 @@ public void setResponseTime(Duration duration) { public Duration getReponseTime() { return responseTime; } + + /** + * Compares only the actual contents of the response when creating a hashcode. + * This was code automatically generated using Elcipse. + */ + @Override + public int hashCode() { + final int prime = 31; + int result = 1; + result = prime * result + ((jsonResponse == null) ? 0 : jsonResponse.hashCode()); + return result; + } + + /** + * Compares only the actual contents of the response. Is also used by contains(). + * This was code automatically generated using Elcipse. + */ + @Override + public boolean equals(Object obj) { + if (this == obj) + return true; + if (obj == null) + return false; + if (getClass() != obj.getClass()) + return false; + Response other = (Response) obj; + if (jsonResponse == null) { + if (other.jsonResponse != null) + return false; + } + else if (!jsonResponse.equals(other.jsonResponse)) + return false; + return true; + } + + /** + * @param s CharSequence + * @return boolean + * @see java.lang.String#contains(java.lang.CharSequence) + */ + public boolean contains(CharSequence s) { + return jsonResponse.contains(s); + } + } diff --git a/src/main/java/com/dougnoel/sentinel/apis/ResponseSet.java b/src/main/java/com/dougnoel/sentinel/apis/ResponseSet.java new file mode 100644 index 00000000..3d565b8f --- /dev/null +++ b/src/main/java/com/dougnoel/sentinel/apis/ResponseSet.java @@ -0,0 +1,48 @@ +package com.dougnoel.sentinel.apis; + +import java.util.ArrayList; + +/** + * Stores a set of responses in order so that they can be retrieved based on when they were created. + * @author dougnoel@gmail.com + * + */ +public class ResponseSet { + protected ArrayList responses = new ArrayList(); + + /** + * Adds a response object to the list of responses we are tracking. + * @param response the response to store + */ + public void add(Response response) { + responses.add(response); + } + + /** + * Returns the most recent response added to the list. + * @return Response the most recent response added to the list + */ + public Response getLatest() { + return responses.get(-1); + } + + /** + * Returns the response added before the most recent response added. + * Used for comparing the current response to the previous one made. + * @return Response the most recent response added to the list + */ + public Response getPrevious() { + return responses.get(-2); + } + + /** + * Returns the response stored based on a 0-based index. + *
0 returns the first response. + *
-1 returns the last response. + * @param index int the order the response was tored in the list + * @return rEsponse the requested response from the list + */ + public Response get(int index) { + return responses.get(index); + } +} \ No newline at end of file diff --git a/src/main/java/com/dougnoel/sentinel/steps/APISteps.java b/src/main/java/com/dougnoel/sentinel/steps/APISteps.java index a7dc701b..9a904959 100644 --- a/src/main/java/com/dougnoel/sentinel/steps/APISteps.java +++ b/src/main/java/com/dougnoel/sentinel/steps/APISteps.java @@ -14,6 +14,9 @@ import com.dougnoel.sentinel.configurations.Configuration; import com.dougnoel.sentinel.enums.RequestType; import com.dougnoel.sentinel.strings.SentinelStringUtils; + +import io.cucumber.java.Before; +import io.cucumber.java.Scenario; import io.cucumber.java.en.Given; import io.cucumber.java.en.Then; import io.cucumber.java.en.When; @@ -21,6 +24,11 @@ public class APISteps { private static final Logger log = LogManager.getLogger(APISteps.class.getName()); // Create a logger. + @Before + public void before(Scenario scenario) { + APIManager.setScenario(scenario); + } + /** * Loads an API based on the environment you are currently testing. * Refer to the documentation in the sentinel.example project for more information. @@ -41,7 +49,30 @@ public void setAPI(String apiName) { /** * Sets the body of the active API call to the string passed. - * + *

+ * Gherkin Examples: + * I set the request body to + * """ + * { + * "id": 10, + * "name": "puppy", + * "category": { + * "id": 1, + * "name": "Dogs" + * }, + * "photoUrls": [ + * "string" + * ], + * "tags": [ + * { + * "id": 0, + * "name": "string" + * } + * ], + * "status": "available" + * } + * """ + * * @param body String the json to be passed as the body of the request. */ @Given("I set the request body to") @@ -53,6 +84,13 @@ public void setRequestBody(String body) { /** * Loads the indicated testdata located in the API object yaml file to use * as the json for the body of the request. + * + *

+ * Gherkin Examples: + *

    + *
  • I load puppydata to use as the request body
  • + *
+ *

* * @param testdataName String the name of the testdata entry to use */ @@ -90,12 +128,12 @@ public void addParameter(String parameter, String value) { *

    *
  • I send a GET request to the pet/findByStatus endpoint
  • *
  • I send a POST request to the users endpoint
  • - *
  • I send a PUT request to the amdins endpoint
  • + *
  • I send a PUT request to the admin endpoint
  • *
*

* - * @param apiCallType - * @param endpoint + * @param apiCallType String the type of call to make + * @param endpoint String the endpoint name as referenced in the swagger file */ @When("^I send a (DELETE|GET|POST|PUT) request to the (.*?) endpoint$") public void sendRequest(String apiCallType, String endpoint) { @@ -132,11 +170,18 @@ public void sendRequest(String apiCallType, String parameter, String endpoint) { public void verifyResponseCodeEquals(int statusCode) { Response response = APIManager.getResponse(); int responseCode = response.getResponseCode(); - var expectedResult = SentinelStringUtils.format("Expected the response code to be {}, and it was {}.\nFull response:\n{}", - statusCode, responseCode, response.getResponse()); + var expectedResult = SentinelStringUtils.format("Expected the response code to be {}, and it was {}.", + statusCode, responseCode); assertTrue(expectedResult, statusCode == responseCode); } + /** + * Validate whether the last response took less time to return than the time given. + * The time is passed as a decimal value (double) expressed as a number of seconds and/or + * fractions of a second. + * + * @param time double the time for comparison in seconds and/or fractions of a second + */ @When("^I verify the response was received in less than (\\d{1,2}(?:[.,]\\d{1,4})?) seconds?$") public void verifyResponseTime(double time) { Duration timeLimit = Duration.ofMillis((long) (time * 1000)); @@ -146,37 +191,46 @@ public void verifyResponseTime(double time) { time, responseTime); assertTrue(expectedResult, responseTime.compareTo(timeLimit) < 0); } - + /** - * Validates text in an API response. - * + * Validates text in an API response against given text, a previous response , or a value entered in a previous step. + *

+ * Gherkin Examples: + *

    + *
  • I validate the response contains the text puppydog
  • + *
  • I validate the response matches the response from the pets endpoint
  • + *
  • I validate the response contains the same text used in Name Field
  • + *
+ *

* @param assertion String null to see if the text exists, "does not" to see if it is absent * @param matchType String use "contains" for a partial match otherwise it will be an exact match * @param text String the text to match */ - @Then("^I validate the response( does not)? (has|have|contains?) the text \"([^\"]*)\"$") - public void verifyResponseContains(String assertion, String matchType, String text) { + @Then("^I validate the response( does not)? (matches?|contains?) (?:the text (.*?)|the response from the (.*?) endpoint|the same text (?:entered|selected|used) for the (.*?))$") + public void verifyResponseContains(String assertion, String matchType, String text, String endpoint, String key) { boolean negate = !StringUtils.isEmpty(assertion); boolean partialMatch = matchType.contains("contain"); - int responseCode = APIManager.getResponse().getResponseCode(); - String responseText = APIManager.getResponse().getResponse(); + Response response = APIManager.getResponse(); + int responseCode = response.getResponseCode(); String expectedResult = SentinelStringUtils.format( - "Expected the response to {}{} the text {}. The response had a response code of {} and contained the text: {}", - (negate ? "not " : ""), (partialMatch ? "contain" : "exactly match"), text, responseCode, responseText - .replace("\n", " ")); - log.trace(expectedResult); + "Expected the response to {}{} the text {}. The response had a response code of {}. " + + "To get the full text of the json response turn on trace logging and look in the logs.", + (negate ? "not " : ""), (partialMatch ? "contain" : "exactly match"), text, responseCode); + log.trace("Expected the response to {}{} the text {}. The response had a response code of {} and contained the text: {}", + (negate ? "not " : ""), (partialMatch ? "contain" : "exactly match"), text, responseCode, response); if (partialMatch) { if (negate) { - assertFalse(expectedResult, responseText.contains(text)); + assertFalse(expectedResult, response.contains(text)); } else { - assertTrue(expectedResult, responseText.contains(text)); + assertTrue(expectedResult, response.contains(text)); } } else { + Response storedResponse = APIManager.getResponse(endpoint); if (negate) { - assertFalse(expectedResult, StringUtils.equals(responseText, text)); + assertFalse(expectedResult, response.equals(storedResponse)); } else { - assertTrue(expectedResult, StringUtils.equals(responseText, text)); + assertTrue(expectedResult, response.equals(storedResponse)); } } } diff --git a/src/main/java/com/dougnoel/sentinel/system/FileManager.java b/src/main/java/com/dougnoel/sentinel/system/FileManager.java index 1cf39b00..aa43b951 100644 --- a/src/main/java/com/dougnoel/sentinel/system/FileManager.java +++ b/src/main/java/com/dougnoel/sentinel/system/FileManager.java @@ -204,10 +204,23 @@ private static String getImagePath(String subDirectory, String fileName) { return convertPathSeparators(Configuration.toString("imageDirectory", IMAGE_DIRECTORY) + File.separator + outputSubDir + File.separator + fileName); } + /** + * Takes a string constructed path, and replaces forward separators with the operating system agnostic File.separator. + * + * @param path String a constructed string path utilizing forward slashes + * @return String a string using OS agnostic File.separator separators + */ public static String convertPathSeparators(String path) { return path.replace("/", File.separator); } + /** + * Performs sanitization on a string to remove all but alphanumeric characters, dashes, and periods. + * Replaces said characters with underscores, resulting in a string which will not violate any system's special character restraints. + * + * @param toSanitize String the string to sanitize + * @return String a string with all non alphanumeric, period, and dashes replaced with underscores + */ public static String sanitizeString(String toSanitize) { return toSanitize.replaceAll("[^a-zA-Z0-9\\.\\-]", "_"); }