Skip to content

Commit

Permalink
Merge pull request #60 from Ragin-LundF/develop
Browse files Browse the repository at this point in the history
Release polling 1.30.0
  • Loading branch information
Ragin-LundF authored Mar 13, 2021
2 parents d582567 + d46ea88 commit 2a56413
Show file tree
Hide file tree
Showing 11 changed files with 395 additions and 3 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/gradle.yml
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ on:
push:
pull_request:
branches:
- master
- main
- develop

jobs:
Expand Down
71 changes: 71 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,74 @@
# Release 1.30.0
## Support for polling APIs
The polling configuration is automatically reset before each scenario.
It can be configured via the background or directly in the scenario.

When the expected HTTP status code and JSON structure has been sent as a response, polling will stop.
This allows an endpoint to be polled until it changes state or fail if the state has not changed during the specified time and retry configuration.

The configuration can be done in to ways.

As a single line configuration:
```gherkin
Scenario: Single line configuration
Given that a request polls every 1 seconds for 5 times
```

Or as a multiline configuration that supports to specify one of the configurations in the `Background` and the other in the `Scenario` (or to have it more readable).

```gherkin
Scenario: Multiline configuration
Given that a requests polls every 1 seconds
And that a requests polls for 5 times
```

The `URL`/`URI` and (if required) body have to be preconfigured. Polling itself does simply use previous set body and path definition.
To execute a request it supports the well known authorized and unauthorized phrases and it supports direct JSON or JSON from file:

Authorized request with JSON response file:
```gherkin
Scenario: Authorized request with JSON response file
Given that a request polls every 1 seconds for 5 times
And that the API path is "/api/v1/polling"
When executing an authorized GET poll request until the response code is 200 and the body is equal to file "expected.json"
```

Unauthorized request with JSON response file:
```gherkin
Scenario: Unauthorized request with JSON response file
Given that a request polls every 1 seconds for 5 times
And that the API path is "/api/v1/polling"
When executing a GET poll request until the response code is 200 and the body is equal to file "expected.json"
```

Authorized request with direct JSON response:
```gherkin
Scenario: Authorized request with JSON response file
Given that a request polls every 1 seconds for 5 times
And that the API path is "/api/v1/polling"
When executing an authorized GET poll request until the response code is 200 and the body is equal to
"""
{
"message": "SUCCESSFUL"
}
"""
```

Unauthorized request with direct JSON response:
```gherkin
Scenario: Unauthorized request with JSON response file
Given that a request polls every 1 seconds for 5 times
And that the API path is "/api/v1/polling"
When executing a GET poll request until the response code is 200 and the body is equal to
"""
{
"message": "SUCCESSFUL"
}
"""
```

Examples can be found at [src/test/resources/features/polling/](src/test/resources/features/polling/).

# Release 1.29.1

## Support for database-less applications
Expand Down
73 changes: 73 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@ dependencies {
- [REST](#rest)
- [JSON-Unit](#json-unit)
- [Adding own pattern for `${json-unit.matches:isValidDate}`](#adding-own-pattern-for-json-unitmatchesisvaliddate)
- [Polling](#polling)
- [Given](#given-1)
- [Define user(s)](#define-users)
- [Set path base directory for request/result/database files](#set-path-base-directory-for-requestresultdatabase-files)
Expand Down Expand Up @@ -376,6 +377,78 @@ Example:
- [Configuration context](src/test/java/com/ragin/bdd/cucumbertests/hooks/CreateContextHooks.java)
- [Test feature](src/test/resources/features/body_validation/field_compare.feature)

### Polling
Polling support combines some `Given` and `When` definitions. For this reason it has its own chapter.

The polling configuration is automatically reset before each scenario.
It can be configured via the background or directly in the scenario.

When the expected HTTP status code and JSON structure has been sent as a response, polling will stop.
This allows an endpoint to be polled until it changes state or fail if the state has not changed during the specified time and retry configuration.

The configuration can be done in to ways.

As a single line configuration:
```gherkin
Scenario: Single line configuration
Given that a request polls every 1 seconds for 5 times
```

Or as a multiline configuration that supports to specify one of the configurations in the `Background` and the other in the `Scenario` (or to have it more readable).

```gherkin
Scenario: Multiline configuration
Given that a requests polls every 1 seconds
And that a requests polls for 5 times
```

The `URL`/`URI` and (if required) body have to be preconfigured. Polling itself does simply use previous set body and path definition.
To execute a request it supports the well known authorized and unauthorized phrases and it supports direct JSON or JSON from file:

Authorized request with JSON response file:
```gherkin
Scenario: Authorized request with JSON response file
Given that a request polls every 1 seconds for 5 times
And that the API path is "/api/v1/polling"
When executing an authorized GET poll request until the response code is 200 and the body is equal to file "expected.json"
```

Unauthorized request with JSON response file:
```gherkin
Scenario: Unauthorized request with JSON response file
Given that a request polls every 1 seconds for 5 times
And that the API path is "/api/v1/polling"
When executing a GET poll request until the response code is 200 and the body is equal to file "expected.json"
```

Authorized request with direct JSON response:
```gherkin
Scenario: Authorized request with JSON response file
Given that a request polls every 1 seconds for 5 times
And that the API path is "/api/v1/polling"
When executing an authorized GET poll request until the response code is 200 and the body is equal to
"""
{
"message": "SUCCESSFUL"
}
"""
```

Unauthorized request with direct JSON response:
```gherkin
Scenario: Unauthorized request with JSON response file
Given that a request polls every 1 seconds for 5 times
And that the API path is "/api/v1/polling"
When executing a GET poll request until the response code is 200 and the body is equal to
"""
{
"message": "SUCCESSFUL"
}
"""
```

Examples can be found at [src/test/resources/features/polling/](src/test/resources/features/polling/).


### Given

Expand Down
4 changes: 2 additions & 2 deletions gradle.properties
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
group=com.ragin.bdd
version=1.29.2
version=1.30.0

systemProp.sonar.host.url=https://sonarcloud.io/
systemProp.sonar.organization=ragin-lundf-github
systemProp.sonar.projectKey=Ragin-LundF_bbd-cucumber-gherkin-lib

versionJavaxJson=1.1.4
versionJsonUnit=2.24.0
versionCucumber=6.9.1
versionCucumber=6.10.1
versionValidationApi=2.0.1.Final
versionCommonsText=1.9
versionCommonsIo=2.8.0
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ object ScenarioStateContext {
var userTokenMap: HashMap<String, String> = HashMap()
private var jsonPathOptions: MutableList<Option> = ArrayList(0)
var executionTime = -1L
var polling = Polling()

/**
* Add IGNORING_EXTRA_ARRAY_ITEMS option to the jsonPathOptions
Expand Down Expand Up @@ -51,6 +52,7 @@ object ScenarioStateContext {
headerValues = HashMap()
jsonPathOptions = ArrayList(0)
bearerToken = defaultBearerToken
polling = Polling()
}

fun getJsonPathOptions(): List<Option> {
Expand All @@ -65,4 +67,9 @@ object ScenarioStateContext {
fun current() : ScenarioStateContext {
return this
}

class Polling {
var pollEverySeconds : Long = 0
var numberOfPolls : Int = -1
}
}
16 changes: 16 additions & 0 deletions src/main/kotlin/com/ragin/bdd/cucumber/glue/GivenRESTStateGlue.kt
Original file line number Diff line number Diff line change
Expand Up @@ -172,4 +172,20 @@ class GivenRESTStateGlue(jsonUtils: JsonUtils) : BaseCucumberCore(jsonUtils) {
ScenarioStateContext.bearerToken = bearerFromContext
}
}

@Given("that a requests polls every {int} seconds")
fun givenPollingSeconds(seconds: Long) {
ScenarioStateContext.polling.pollEverySeconds = seconds
}

@Given("that a requests polls for {int} times")
fun givenPollingNumberOfPolls(numberOfPolls: Int) {
ScenarioStateContext.polling.numberOfPolls = numberOfPolls
}

@Given("that a request polls every {int} seconds for {int} times")
fun givenPollingSecondsAndNumberOfPolls(seconds: Long, numberOfPolls: Int) {
ScenarioStateContext.polling.pollEverySeconds = seconds
ScenarioStateContext.polling.numberOfPolls = numberOfPolls
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import java.io.IOException
* This class contains HTTP response validations.
*/
class ThenRESTValidationGlue(jsonUtils: JsonUtils) : BaseCucumberCore(jsonUtils) {

/**
* Ensure, that the response code is valid
* @param expectedStatusCode HTTP status code that is expected
Expand Down
101 changes: 101 additions & 0 deletions src/main/kotlin/com/ragin/bdd/cucumber/glue/WhenRESTExecutionGlue.kt
Original file line number Diff line number Diff line change
@@ -1,19 +1,28 @@
package com.ragin.bdd.cucumber.glue

import com.ragin.bdd.cucumber.core.ScenarioStateContext
import com.ragin.bdd.cucumber.core.ScenarioStateContext.editableBody
import com.ragin.bdd.cucumber.core.ScenarioStateContext.uriPath
import com.ragin.bdd.cucumber.utils.JsonUtils
import io.cucumber.datatable.DataTable
import io.cucumber.java.ParameterType
import io.cucumber.java.en.Then
import io.cucumber.java.en.When
import org.apache.commons.logging.LogFactory
import org.junit.Assert
import org.springframework.boot.test.web.client.TestRestTemplate
import org.springframework.http.HttpMethod
import java.io.IOException
import java.util.concurrent.TimeUnit

/**
* This class contains common `When` execution of REST related steps.
*/
class WhenRESTExecutionGlue(jsonUtils: JsonUtils, restTemplate: TestRestTemplate) : BaseRESTExecutionGlue(jsonUtils, restTemplate) {
companion object {
private val log = LogFactory.getLog(ThenRESTValidationGlue::class.java)
}

/**
* Execute a {httpMethod} call with previously given URI and body
*
Expand Down Expand Up @@ -263,6 +272,98 @@ class WhenRESTExecutionGlue(jsonUtils: JsonUtils, restTemplate: TestRestTemplate
executeRequest(dataTable, httpMethod, false)
}

/**
* Execute an authorized poll request, which has to be preconfigured until the response contains
* the status and response message from a file.
*
* @param httpMethod HTTP Method
* @param expectedStatusCode the expected HTTP status code
* @param pathToFile describes the path to the expected JSON response file
*/
@Then("executing an authorized {httpMethod} poll request until the response code is {int} and the body is equal to file {string}")
@Throws(IOException::class)
fun whenExecutingAuthorizedPollingUntilResponseIsEqualToFile(httpMethod: HttpMethod,
expectedStatusCode: Int,
pathToFile: String
) {
val expectedBody = readFile(pathToFile)
executePollRequestUntilResponseIsEqual(httpMethod, expectedStatusCode, expectedBody, true)
}

/**
* Execute a poll request, which has to be preconfigured until the response contains
* the status and response message from a file.
*
* @param httpMethod HTTP Method
* @param expectedStatusCode the expected HTTP status code
* @param pathToFile describes the path to the expected JSON response file
*/
@Then("executing a {httpMethod} poll request until the response code is {int} and the body is equal to file {string}")
@Throws(IOException::class)
fun whenExecutingPollingUntilResponseIsEqualToFile(httpMethod: HttpMethod,
expectedStatusCode: Int,
pathToFile: String
) {
val expectedBody = readFile(pathToFile)
executePollRequestUntilResponseIsEqual(httpMethod, expectedStatusCode, expectedBody, false)
}

/**
* Execute an authorized poll request, which has to be preconfigured until the response contains
* the status and JSON response message.
*
* @param httpMethod HTTP Method
* @param expectedStatusCode the expected HTTP status code
* @param expectedBody describes the expected JSON body
*/
@Then("executing an authorized {httpMethod} poll request until the response code is {int} and the body is equal to")
fun whenExecutingAuthorizedPollingUntilResponseIsEqual(httpMethod: HttpMethod, expectedStatusCode: Int, expectedBody: String) {
executePollRequestUntilResponseIsEqual(httpMethod, expectedStatusCode, expectedBody, true)
}

/**
* Execute a poll request, which has to be preconfigured until the response contains
* the status and JSON response message.
*
* @param httpMethod HTTP Method
* @param expectedStatusCode the expected HTTP status code
* @param expectedBody describes the expected JSON body
*/
@Then("executing a {httpMethod} poll request until the response code is {int} and the body is equal to")
fun whenExecutingPollingUntilResponseIsEqual(httpMethod: HttpMethod, expectedStatusCode: Int, expectedBody: String) {
executePollRequestUntilResponseIsEqual(httpMethod, expectedStatusCode, expectedBody, false)
}

private fun executePollRequestUntilResponseIsEqual(httpMethod: HttpMethod, expectedStatusCode: Int, expectedBody: String, authorized: Boolean) {
Assert.assertNotEquals("Please configure max number of polls!",
-1,
ScenarioStateContext.polling.numberOfPolls
)

var repeatLoop = 0
for (i in 1..ScenarioStateContext.polling.numberOfPolls) {
executeRequest(httpMethod, authorized)
try {
assertJSONisEqual(
expectedBody,
ScenarioStateContext.latestResponse!!.body
)
Assert.assertEquals(expectedStatusCode, ScenarioStateContext.latestResponse!!.statusCode.value())
repeatLoop = i
break
} catch (error: AssertionError) {
TimeUnit.SECONDS.sleep(ScenarioStateContext.polling.pollEverySeconds)
}
}

assertJSONisEqual(
expectedBody,
ScenarioStateContext.latestResponse!!.body
)
Assert.assertEquals(expectedStatusCode, ScenarioStateContext.latestResponse!!.statusCode.value())
log.info("Polling finished after $repeatLoop repeats")
}

/**
* Definition of {httpMethod} to offer concrete but dynamic parameter type
*
Expand Down
Loading

0 comments on commit 2a56413

Please sign in to comment.