Skip to content

Commit

Permalink
Merge pull request #103 from christophd/issue/101/wait-for-resource-path
Browse files Browse the repository at this point in the history
fix(#101): Wait for Http resource path
  • Loading branch information
christophd authored May 5, 2020
2 parents 6b11489 + 12ef8d6 commit 87cca85
Show file tree
Hide file tree
Showing 4 changed files with 120 additions and 20 deletions.
58 changes: 57 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand All @@ -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);
Expand Down Expand Up @@ -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 )\"(.+)\"$")
Expand Down Expand Up @@ -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;
}

Expand All @@ -374,7 +403,7 @@ public void setInboundDictionary(DataDictionary inboundDictionary) {
*
* @param outboundDictionary
*/
public void setOutboundDictionary(DataDictionary outboundDictionary) {
public void setOutboundDictionary(DataDictionary<?> outboundDictionary) {
this.outboundDictionary = outboundDictionary;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand All @@ -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)) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down

0 comments on commit 87cca85

Please sign in to comment.