From 12ef8d6534e689f1bf70a432c4f64cacc3d193e5 Mon Sep 17 00:00:00 2001 From: Christoph Deppisch Date: Mon, 4 May 2020 10:23:20 +0200 Subject: [PATCH] fix(#101): Wait for Http resource path - Support both full request URL and relative resource path when doing a Http health check - Add Http steps documentation to README - Fix http-server and http-client step wording to be compliant with other steps - Add "HTTP server listening on port {port}" step to create new server instance --- README.md | 58 ++++++++++++++++- .../yaks/http/HttpClientSteps.java | 63 ++++++++++++++----- .../yaks/http/HttpServerSteps.java | 11 +++- .../yaks/http/http.client.feature | 8 ++- 4 files changed, 120 insertions(+), 20 deletions(-) diff --git a/README.md b/README.md index d8eb76a7..bc23f237 100644 --- a/README.md +++ b/README.md @@ -14,7 +14,7 @@ YAKS Cloud-Native BDD testing or simply: Yet Another Kubernetes Service * Apache Camel Steps * [Camel K Steps](#camel-k-steps) * [JDBC Steps](#jdbc-steps) - * Http Steps + * [Http Steps](#http-steps) * [Open API Steps](#openapi-steps) * Kafka Steps * Jms Steps @@ -282,6 +282,62 @@ You can find examples of JDBC steps in the [examples](/examples/jdbc.feature) fi There's also an example that uses [JDBC and REST together](/examples/task-api.feature) and targets the [Syndesis TODO App](https://github.com/syndesisio/todo-example) database. +### Http steps + +The Http protocol is a widely used communication protocol when it comes to exchanging data between systems. REST Http services +are very prominent and producing/consuming those services is a common task in software development these days. YAKS provides +ready to use steps that are able to exchange request/response messages via Http during the test. + +As a client you can specify the server URL and send requests to it. + +```gherkin +Feature: Http client + + Background: + Given URL: http://localhost:8080 + + Scenario: Health check + Given path /health is healthy + + Scenario: GET request + When send GET /todo + Then verify HTTP response body: {"id": "@ignore@", "task": "Sample task", "completed": 0} + And receive HTTP 200 OK +``` + +The example above sets a base request URL to `http://localhost:8080` and performs a health check on path `/health`. After that we can +send any request to the server and verify the response body and status code. + +All these steps are part of the core YAKS framework and you can just use them. + +On the server side we can start a new Http server instance on a given port and listen for incoming requests. These request can be verified and +the test can provide a simulated response message with body and header data. + +```gherkin +Feature: Http server + + Background: + Given HTTP server listening on port 8080 + + Scenario: Expect GET request + When receive GET /todo + Then HTTP response body: {"id": 1000, "task": "Sample task", "completed": 0} + And send HTTP 200 OK + + Scenario: Expect POST request + Given expect HTTP request body: {"id": "@isNumber()@", "task": "New task", "completed": "@matches(0|1)@"} + When receive POST /todo + Then send HTTP 201 CREATED +``` + +In the HTTP server sample above we create a new server instance listening on port `8080`. Then we expect a `GET` request on path `/todo`. The server responds with +a Http `200 OK` response message and given Json body as payload. + +The second scenario expects a POST request with a given body as Json payload. The expected request payload is verified with the powerful Citrus JSON +message validator being able to compare JSON tree structures in combination with validation matchers such as `isNumber()` or `matches(0|1)`. + +Once the request is verified the server responds with a simple Http `201 CREATED`. + ### OpenAPI steps OpenAPI documents specify RESTful Http services in a standardized, language-agnostic way. The specifications describe diff --git a/java/steps/yaks-http/src/main/java/org/citrusframework/yaks/http/HttpClientSteps.java b/java/steps/yaks-http/src/main/java/org/citrusframework/yaks/http/HttpClientSteps.java index 54eaea5f..59eb5975 100644 --- a/java/steps/yaks-http/src/main/java/org/citrusframework/yaks/http/HttpClientSteps.java +++ b/java/steps/yaks-http/src/main/java/org/citrusframework/yaks/http/HttpClientSteps.java @@ -83,8 +83,8 @@ public class HttpClientSteps implements HttpSteps { private String requestBody; private String responseBody; - private DataDictionary outboundDictionary; - private DataDictionary inboundDictionary; + private DataDictionary outboundDictionary; + private DataDictionary inboundDictionary; private long timeout; @@ -110,7 +110,7 @@ public void before(Scenario scenario) { inboundDictionary = null; } - @Given("^http-client \"([^\"\\s]+)\"$") + @Given("^HTTP client \"([^\"\\s]+)\"$") public void setClient(String id) { if (!citrus.getCitrusContext().getReferenceResolver().isResolvable(id)) { throw new CitrusRuntimeException("Unable to find http client for id: " + id); @@ -138,33 +138,38 @@ public void healthCheck() { waitForHttpUrl(requestUrl); } - @Given("^wait for (?:URL|url) ([^\\s]+)$") - public void waitForHttpUrl(String url) { - waitForHttpStatus(url, 200); + @Given("^(?:URL|url|path) ([^\\s]+) is healthy$") + public void healthCheck(String urlOrPath) { + waitForHttpUrl(getRequestUrl(urlOrPath)); } - @Given("^wait for (GET|HEAD|POST|PUT|PATCH|DELETE|OPTIONS|TRACE) on (?:URL|url) ([^\\s]+)$") - public void waitForHttpUrlUsingMethod(String method, String url) { - waitForHttpStatusUsingMethod(method, url, 200); + @Given("^wait for (?:URL|url|path) ([^\\s]+)$") + public void waitForHttpUrl(String urlOrPath) { + waitForHttpStatus(getRequestUrl(urlOrPath), 200); } - @Given("^wait for (?:URL|url) ([^\\s]+) to return (\\d+)(?: [^\\s]+)?$") - public void waitForHttpStatus(String url, Integer statusCode) { + @Given("^wait for (GET|HEAD|POST|PUT|PATCH|DELETE|OPTIONS|TRACE) on (?:URL|url|path) ([^\\s]+)$") + public void waitForHttpUrlUsingMethod(String method, String urlOrPath) { + waitForHttpStatusUsingMethod(method, getRequestUrl(urlOrPath), 200); + } + + @Given("^wait for (?:URL|url|path) ([^\\s]+) to return (\\d+)(?: [^\\s]+)?$") + public void waitForHttpStatus(String urlOrPath, Integer statusCode) { runner.given(Wait.Builder.waitFor().http() .milliseconds(timeout) .interval(timeout / 10) .status(statusCode) - .url(url)); + .url(getRequestUrl(urlOrPath))); } - @Given("^wait for (GET|HEAD|POST|PUT|PATCH|DELETE|OPTIONS|TRACE) on (?:URL|url) ([^\\s]+) to return (\\d+)(?: [^\\s]+)?$") - public void waitForHttpStatusUsingMethod(String method, String url, Integer statusCode) { + @Given("^wait for (GET|HEAD|POST|PUT|PATCH|DELETE|OPTIONS|TRACE) on (?:URL|url|path) ([^\\s]+) to return (\\d+)(?: [^\\s]+)?$") + public void waitForHttpStatusUsingMethod(String method, String urlOrPath, Integer statusCode) { runner.given(Wait.Builder.waitFor().http() .milliseconds(timeout) .method(method) .interval(timeout / 10) .status(statusCode) - .url(url)); + .url(getRequestUrl(urlOrPath))); } @Then("^(?:expect|verify) HTTP response header ([^\\s]+)(?:=| is )\"(.+)\"$") @@ -360,12 +365,36 @@ private org.apache.http.client.HttpClient sslClient() { } } + /** + * Helper method concatenating base request URL and given relative URL resource path. In case given parameter us a full qualified + * URL itself use this URL as a result. Adds error handling in case base request URL is not set properly and avoids duplicate path + * separators in concatenated URLs. + * + * @param urlOrPath + * @return + */ + private String getRequestUrl(String urlOrPath) { + if (StringUtils.hasText(urlOrPath) && urlOrPath.startsWith("http")) { + return urlOrPath; + } + + if (!StringUtils.hasText(requestUrl)) { + throw new IllegalStateException("Must provide a base request URL first when using relative resource path: " + urlOrPath); + } + + if (!StringUtils.hasText(urlOrPath) || urlOrPath.equals("/")) { + return requestUrl; + } + + return (requestUrl.endsWith("/") ? requestUrl : requestUrl + "/") + (urlOrPath.startsWith("/") ? urlOrPath.substring(1) : urlOrPath); + } + /** * Specifies the inboundDictionary. * * @param inboundDictionary */ - public void setInboundDictionary(DataDictionary inboundDictionary) { + public void setInboundDictionary(DataDictionary inboundDictionary) { this.inboundDictionary = inboundDictionary; } @@ -374,7 +403,7 @@ public void setInboundDictionary(DataDictionary inboundDictionary) { * * @param outboundDictionary */ - public void setOutboundDictionary(DataDictionary outboundDictionary) { + public void setOutboundDictionary(DataDictionary outboundDictionary) { this.outboundDictionary = outboundDictionary; } } diff --git a/java/steps/yaks-http/src/main/java/org/citrusframework/yaks/http/HttpServerSteps.java b/java/steps/yaks-http/src/main/java/org/citrusframework/yaks/http/HttpServerSteps.java index c8d217ad..0aaa1be2 100644 --- a/java/steps/yaks-http/src/main/java/org/citrusframework/yaks/http/HttpServerSteps.java +++ b/java/steps/yaks-http/src/main/java/org/citrusframework/yaks/http/HttpServerSteps.java @@ -85,7 +85,7 @@ public void before(Scenario scenario) { bodyValidationExpressions = new HashMap<>(); } - @Given("^http-server \"([^\"\\s]+)\"$") + @Given("^HTTP server \"([^\"\\s]+)\"$") public void setServer(String id) { if (!citrus.getCitrusContext().getReferenceResolver().isResolvable(id)) { throw new CitrusRuntimeException("Unable to find http server for id: " + id); @@ -94,6 +94,15 @@ public void setServer(String id) { httpServer = citrus.getCitrusContext().getReferenceResolver().resolve(id, HttpServer.class); } + @Given("^HTTP server listening on port (\\d+)$") + public void createServer(int port) { + httpServer = new HttpServerBuilder() + .port(port) + .build(); + + httpServer.start(); + } + @Then("^(?:expect|verify) HTTP request header: ([^\\s]+)(?:=| is )\"(.+)\"$") public void addRequestHeader(String name, String value) { if (name.equals(HttpHeaders.CONTENT_TYPE)) { diff --git a/java/steps/yaks-http/src/test/resources/org/citrusframework/yaks/http/http.client.feature b/java/steps/yaks-http/src/test/resources/org/citrusframework/yaks/http/http.client.feature index 4c74467d..345c47a1 100644 --- a/java/steps/yaks-http/src/test/resources/org/citrusframework/yaks/http/http.client.feature +++ b/java/steps/yaks-http/src/test/resources/org/citrusframework/yaks/http/http.client.feature @@ -5,12 +5,18 @@ Feature: Http client Given URL: http://localhost:${port} Scenario: Health check - And URL is healthy + Given URL is healthy + And URL http://localhost:${port}/todo is healthy + And path /todo is healthy Scenario: Wait for Http URL condition Given HTTP request timeout is 5000 milliseconds Then wait for URL http://localhost:${port}/todo to return 200 OK + And wait for path /todo to return 200 OK + And wait for GET on URL http://localhost:${port}/todo to return 200 OK + And wait for GET on URL /todo to return 200 OK And wait for GET on URL http://localhost:${port}/todo + And wait for GET on path /todo Scenario: GET When send GET /todo