diff --git a/.github/workflows/build-twofactor.yml b/.github/workflows/build-twofactor.yml new file mode 100644 index 000000000..c0e054d45 --- /dev/null +++ b/.github/workflows/build-twofactor.yml @@ -0,0 +1,42 @@ +name: Fineract Gradle build - twofactor +on: [push, pull_request] + +jobs: + build: + # We need 18.04 for this - the 20.04 image contains MySQL 8.0, and the GitHub Action to install 5.7 is not permitted in the Apache repo. + runs-on: ubuntu-18.04 + env: + TZ: Asia/Kolkata + steps: + - name: Set up cache + uses: actions/cache@v2 + with: + path: | + ~/.gradle/caches + ~/.gradle/wrapper + key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties') }} + restore-keys: | + ${{ runner.os }}-gradle- + - name: Checkout + uses: actions/checkout@v2 + - name: Set up JDK 11 + uses: actions/setup-java@v2 + with: + java-version: '11' + distribution: 'zulu' + - name: Validate Gradle wrapper + uses: gradle/wrapper-validation-action@e6e38bacfdf1a337459f332974bb2327a31aaf4b + - name: Start up MySQL 5.7 + run: | + sudo /etc/init.d/mysql start + echo "USE mysql;\nALTER USER 'root'@'localhost' IDENTIFIED BY 'mysql';\n" | mysql -u root -proot + - name: Initialise databases + run: | + ./gradlew --no-daemon -q createDB -PdbName=fineract_tenants + ./gradlew --no-daemon -q createDB -PdbName=fineract_default + - name: Install additional software + run: | + sudo apt-get update + sudo apt-get install ghostscript -y + - name: Build & Test + run: ./gradlew --no-daemon -q --console=plain licenseMain licenseTest check build test --fail-fast doc -x :integration-tests:test -Ptwofactor=enabled diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index cfcc71c7b..fe53337d3 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -1,4 +1,4 @@ -name: Fineract Gradle build +name: Fineract Gradle build - basicauth on: [push, pull_request] jobs: @@ -39,4 +39,4 @@ jobs: sudo apt-get update sudo apt-get install ghostscript -y - name: Build & Test - run: ./gradlew --no-daemon -q --console=plain licenseMain licenseTest check build test --fail-fast doc + run: ./gradlew --no-daemon -q --console=plain licenseMain licenseTest check build test --fail-fast doc -x :twofactor-tests:test diff --git a/build.gradle b/build.gradle index 37013f1f9..0de0c8bae 100644 --- a/build.gradle +++ b/build.gradle @@ -28,6 +28,7 @@ buildscript { 'fineract-api', 'fineract-provider', 'integration-tests', + 'twofactor-tests', 'fineract-client' ].contains(it.name) } @@ -144,6 +145,7 @@ allprojects { dependency 'org.webjars.npm:swagger-ui-dist:4.0.1' dependency 'org.webjars:webjars-locator-core:0.48' dependency 'org.springframework.boot:spring-boot-starter-mail:2.6.0' + dependency 'com.icegreen:greenmail-junit5:1.6.5' // fineract client dependencies dependency "com.squareup.retrofit2:retrofit:$retrofitVersion" diff --git a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/configuration/service/ExternalServicesPropertiesReadPlatformServiceImpl.java b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/configuration/service/ExternalServicesPropertiesReadPlatformServiceImpl.java index 8bde32705..c66cda977 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/configuration/service/ExternalServicesPropertiesReadPlatformServiceImpl.java +++ b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/configuration/service/ExternalServicesPropertiesReadPlatformServiceImpl.java @@ -83,7 +83,7 @@ public class ExternalServicesPropertiesReadPlatformServiceImpl implements Extern if (ExternalServicesConstants.SMTP_USERNAME.equalsIgnoreCase(name)) { username = value; - } else if (ExternalServicesConstants.SMTP_PORT.equalsIgnoreCase(port)) { + } else if (ExternalServicesConstants.SMTP_PORT.equalsIgnoreCase(name)) { port = value; } else if (ExternalServicesConstants.SMTP_PASSWORD.equalsIgnoreCase(name)) { password = value; diff --git a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/core/service/DateUtils.java b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/core/service/DateUtils.java index 53c6fada1..bd08a6c76 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/core/service/DateUtils.java +++ b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/core/service/DateUtils.java @@ -25,6 +25,7 @@ import java.time.LocalDateTime; import java.time.ZoneId; import java.time.ZonedDateTime; import java.time.format.DateTimeFormatter; +import java.time.temporal.ChronoUnit; import java.util.ArrayList; import java.util.Date; import java.util.List; @@ -69,7 +70,7 @@ public final class DateUtils { public static LocalDateTime getLocalDateTimeOfTenant() { final ZoneId zone = getDateTimeZoneOfTenant(); - LocalDateTime today = LocalDateTime.now(zone); + LocalDateTime today = LocalDateTime.now(zone).truncatedTo(ChronoUnit.SECONDS); return today; } diff --git a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/security/domain/TFAccessToken.java b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/security/domain/TFAccessToken.java index c9165e740..55934757e 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/security/domain/TFAccessToken.java +++ b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/security/domain/TFAccessToken.java @@ -128,6 +128,6 @@ public class TFAccessToken extends AbstractPersistableCustom { } private boolean isDateInThePast(LocalDateTime dateTime) { - return dateTime.isBefore(DateUtils.getLocalDateTimeOfTenant()); + return (dateTime.isBefore(DateUtils.getLocalDateTimeOfTenant()) || dateTime.isEqual(DateUtils.getLocalDateTimeOfTenant())); } } diff --git a/fineract-provider/src/main/resources/sql/migrations/core_db/V265__modify_external_service_schema.sql b/fineract-provider/src/main/resources/sql/migrations/core_db/V265__modify_external_service_schema.sql index b4e8d2c0f..b83799cfb 100644 --- a/fineract-provider/src/main/resources/sql/migrations/core_db/V265__modify_external_service_schema.sql +++ b/fineract-provider/src/main/resources/sql/migrations/core_db/V265__modify_external_service_schema.sql @@ -53,9 +53,9 @@ insert into c_external_service_properties (`name`, `value`, `external_service_id insert into c_external_service_properties (`name`, `value`, `external_service_id`) values('password', 'support80', (select id from c_external_service where name = 'SMTP_Email_Account')); -insert into c_external_service_properties (`name`, `value`, `external_service_id`) values('host', 'smtp.gmail.com', (select id from c_external_service where name = 'SMTP_Email_Account')); +insert into c_external_service_properties (`name`, `value`, `external_service_id`) values('host', 'localhost', (select id from c_external_service where name = 'SMTP_Email_Account')); -insert into c_external_service_properties (`name`, `value`, `external_service_id`) values('port', '587', (select id from c_external_service where name = 'SMTP_Email_Account')); +insert into c_external_service_properties (`name`, `value`, `external_service_id`) values('port', '3025', (select id from c_external_service where name = 'SMTP_Email_Account')); insert into c_external_service_properties (`name`, `value`, `external_service_id`) values('useTLS', 'true', (select id from c_external_service where name = 'SMTP_Email_Account')); diff --git a/settings.gradle b/settings.gradle index fcb6dc03d..596dc4ee9 100644 --- a/settings.gradle +++ b/settings.gradle @@ -19,5 +19,6 @@ rootProject.name='fineract' include ':fineract-provider' include ':integration-tests' +include ':twofactor-tests' include ':fineract-client' include ':fineract-doc' diff --git a/twofactor-tests/build.gradle b/twofactor-tests/build.gradle new file mode 100644 index 000000000..bddfd3d8c --- /dev/null +++ b/twofactor-tests/build.gradle @@ -0,0 +1,67 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +description = 'Fineract Integration Tests' + +apply plugin: 'com.bmuschko.cargo' + +// Configuration for the Gradle Cargo plugin +// https://github.com/bmuschko/gradle-cargo-plugin +configurations { + tomcat +} + +apply from: 'dependencies.gradle' + +cargo { + containerId "tomcat9x" + + // looks like Cargo doesn't detect the WAR file automatically in the multi-module setup + deployable { + file = file("$rootDir/fineract-provider/build/libs/fineract-provider.war") + context = 'fineract-provider' + } + + local { + installer { + installConfiguration = configurations.tomcat + downloadDir = file("$buildDir/download") + extractDir = file("$buildDir/tomcat") + } + startStopTimeout = 240000 + containerProperties { + property 'cargo.start.jvmargs', '-Dspring.profiles.active=twofactor,basicauth' + property 'cargo.tomcat.connector.keystoreFile', file("$rootDir/fineract-provider/src/main/resources/keystore.jks") + property 'cargo.tomcat.connector.keystorePass', 'openmf' + property 'cargo.tomcat.httpSecure', true + property 'cargo.tomcat.connector.sslProtocol', 'TLS' + property 'cargo.tomcat.connector.clientAuth', false + property 'cargo.protocol', 'https' + property 'cargo.servlet.port', 8443 + } + } +} + +cargoRunLocal.dependsOn ':fineract-provider:war' +cargoStartLocal.dependsOn ':fineract-provider:war' +cargoStartLocal.mustRunAfter 'testClasses' + +test { + dependsOn cargoStartLocal + finalizedBy cargoStopLocal +} diff --git a/twofactor-tests/dependencies.gradle b/twofactor-tests/dependencies.gradle new file mode 100644 index 000000000..d99bb864e --- /dev/null +++ b/twofactor-tests/dependencies.gradle @@ -0,0 +1,37 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +dependencies { + // testCompile dependencies are ONLY used in src/test, not src/main. + // Do NOT repeat dependencies which are ALREADY in implementation or runtimeOnly! + // + tomcat 'org.apache.tomcat:tomcat:9.0.54@zip' + testImplementation( files("$rootDir/fineract-provider/build/classes/java/main/"), + project(path: ':fineract-provider', configuration: 'runtimeElements'), + 'org.junit.jupiter:junit-jupiter-api', + 'com.icegreen:greenmail-junit5' + ) + testImplementation ('io.rest-assured:rest-assured') { + exclude group: 'commons-logging' + exclude group: 'org.apache.sling' + exclude group: 'com.sun.xml.bind' + } + testRuntimeOnly( + 'org.junit.jupiter:junit-jupiter-engine' + ) +} diff --git a/twofactor-tests/src/test/java/org/apache/fineract/twofactortests/TwoFactorAuthenticationTest.java b/twofactor-tests/src/test/java/org/apache/fineract/twofactortests/TwoFactorAuthenticationTest.java new file mode 100644 index 000000000..e3703b467 --- /dev/null +++ b/twofactor-tests/src/test/java/org/apache/fineract/twofactortests/TwoFactorAuthenticationTest.java @@ -0,0 +1,304 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.fineract.twofactortests; + +import static io.restassured.RestAssured.given; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotNull; +import static org.junit.jupiter.api.Assertions.fail; + +import com.icegreen.greenmail.configuration.GreenMailConfiguration; +import com.icegreen.greenmail.junit5.GreenMailExtension; +import com.icegreen.greenmail.util.ServerSetupTest; +import io.restassured.RestAssured; +import io.restassured.builder.RequestSpecBuilder; +import io.restassured.builder.ResponseSpecBuilder; +import io.restassured.http.ContentType; +import io.restassured.path.json.JsonPath; +import io.restassured.response.Response; +import io.restassured.specification.RequestSpecification; +import io.restassured.specification.ResponseSpecification; +import java.io.IOException; +import java.util.HashMap; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.regex.Matcher; +import java.util.regex.Pattern; +import javax.mail.MessagingException; +import org.apache.commons.lang3.StringUtils; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +public class TwoFactorAuthenticationTest { + + private ResponseSpecification responseSpec; + private ResponseSpecification responseSpec403; + private ResponseSpecification responseSpec401; + private RequestSpecification requestSpecWithoutBasic; + private RequestSpecification requestSpec; + private String basicAuthenticationKey; + + public static final String TENANT_PARAM_NAME = "tenantIdentifier"; + public static final String DEFAULT_TENANT = "default"; + public static final String TENANT_IDENTIFIER = TENANT_PARAM_NAME + '=' + DEFAULT_TENANT; + private static final String LOGIN_URL = "/fineract-provider/api/v1/authentication?" + TENANT_IDENTIFIER; + private static final String HEALTH_URL = "/fineract-provider/actuator/health"; + + @RegisterExtension + static GreenMailExtension greenMail = new GreenMailExtension(ServerSetupTest.SMTP) + .withConfiguration(GreenMailConfiguration.aConfig().withUser("support@cloudmicrofinance.com", "support81")) + .withPerMethodLifecycle(true); + + @BeforeEach + public void setup() throws InterruptedException { + initializeRestAssured(); + + this.requestSpec = new RequestSpecBuilder().setContentType(ContentType.JSON).build(); + + // Login with basic authentication + awaitSpringBootActuatorHealthyUp(); + String json = RestAssured.given().contentType(ContentType.JSON).body("{\"username\":\"mifos\", \"password\":\"password\"}").expect() + .log().ifError().when().post(LOGIN_URL).asString(); + assertFalse(StringUtils.isBlank(json)); + + this.basicAuthenticationKey = JsonPath.with(json).get("base64EncodedAuthenticationKey"); + assertFalse(StringUtils.isBlank(this.basicAuthenticationKey)); + + this.requestSpec.header("Authorization", "Basic " + basicAuthenticationKey); + + this.requestSpecWithoutBasic = new RequestSpecBuilder().setContentType(ContentType.JSON).build(); + this.responseSpec = new ResponseSpecBuilder().expectStatusCode(200).build(); + this.responseSpec403 = new ResponseSpecBuilder().expectStatusCode(403).build(); + this.responseSpec401 = new ResponseSpecBuilder().expectStatusCode(401).build(); + } + + @Test + public void testActuatorAccess() { + performServerGet(requestSpecWithoutBasic, responseSpec, "/fineract-provider/actuator/info", null); + } + + @Test + public void testApiDocsAccess() { + performServerGet(requestSpecWithoutBasic, responseSpec, "/fineract-provider/api-docs/apiLive.htm", null); + } + + @Test + public void testAccessWithoutTwofactor() { + performServerGet(requestSpec, responseSpec403, "/fineract-provider/api/v1/offices/1?" + TENANT_IDENTIFIER, ""); + } + + @Test + public void testCheckTwofactorEnabled() { + String json = RestAssured.given().contentType(ContentType.JSON).body("{\"username\":\"mifos\", \"password\":\"password\"}").expect() + .log().ifError().when().post("/fineract-provider/api/v1/authentication?" + TENANT_IDENTIFIER).asString(); + assertFalse(StringUtils.isBlank(json)); + Boolean key = JsonPath.with(json).get("isTwoFactorAuthenticationRequired"); + assertEquals(key, true); + } + + @Test + public void testGetTwofactorMethods() { + String json = RestAssured.given().spec(requestSpec).expect().log().ifError().when() + .get("/fineract-provider/api/v1/twofactor?" + TENANT_IDENTIFIER).asString(); + assertFalse(StringUtils.isBlank(json)); + List> twoFactorMethods = JsonPath.with(json).getList("$"); + assertEquals("email", twoFactorMethods.get(0).get("name")); + assertEquals("demomfi@mifos.org", twoFactorMethods.get(0).get("target")); + } + + @Test + public void testTwofactorLogin() throws IOException, MessagingException { + assertEquals(greenMail.getReceivedMessages().length, 0); + performServerPost(requestSpec, responseSpec, + "/fineract-provider/api/v1/twofactor?deliveryMethod=email&extendedToken=false&" + TENANT_IDENTIFIER, "", ""); + assertEquals(greenMail.getReceivedMessages().length, 1); + + Pattern p = Pattern.compile("token is (.+)."); + Matcher m = p.matcher((CharSequence) greenMail.getReceivedMessages()[0].getContent()); + + String token = null; + + while (m.find()) { + token = m.group(1); + } + + assertNotNull(token); + String tfaToken = performServerPost(requestSpec, responseSpec, + "/fineract-provider/api/v1/twofactor/validate?token=" + token + "&" + TENANT_IDENTIFIER, "", "token"); + assertNotNull(tfaToken); + + RequestSpecification requestSpecWithTFA = new RequestSpecBuilder() // + .setContentType(ContentType.JSON) // + .addHeader("Fineract-Platform-TFA-Token", tfaToken) // + .addHeader("Authorization", "Basic " + basicAuthenticationKey) // + .build(); + + performServerGet(requestSpecWithTFA, responseSpec, "/fineract-provider/api/v1/offices/1?" + TENANT_IDENTIFIER, ""); + + performServerPost(requestSpecWithTFA, responseSpec, "/fineract-provider/api/v1/twofactor/invalidate?" + TENANT_IDENTIFIER, + "{ \"token\": \"" + tfaToken + "\" }", ""); + + performServerGet(requestSpecWithTFA, responseSpec401, "/fineract-provider/api/v1/offices/1?" + TENANT_IDENTIFIER, ""); + } + + @Test + public void testTfaConfigSettings() throws IOException, MessagingException { + assertEquals(greenMail.getReceivedMessages().length, 0); + performServerPost(requestSpec, responseSpec, + "/fineract-provider/api/v1/twofactor?deliveryMethod=email&extendedToken=false&" + TENANT_IDENTIFIER, "", ""); + assertEquals(greenMail.getReceivedMessages().length, 1); + + Pattern p = Pattern.compile("token is (.+)."); + Matcher m = p.matcher((CharSequence) greenMail.getReceivedMessages()[0].getContent()); + + String token = null; + + while (m.find()) { + token = m.group(1); + } + + assertNotNull(token); + String tfaToken = performServerPost(requestSpec, responseSpec, + "/fineract-provider/api/v1/twofactor/validate?token=" + token + "&" + TENANT_IDENTIFIER, "", "token"); + assertNotNull(tfaToken); + + RequestSpecification requestSpecWithTFA = new RequestSpecBuilder() // + .setContentType(ContentType.JSON) // + .addHeader("Fineract-Platform-TFA-Token", tfaToken) // + .addHeader("Authorization", "Basic " + basicAuthenticationKey) // + .build(); + + // Get the configuration and check one of the values (OTP token length) + LinkedHashMap json = performServerGet(requestSpecWithTFA, responseSpec, + "/fineract-provider/api/v1/twofactor/configure?" + TENANT_IDENTIFIER, ""); + assertEquals(json.get("otp-token-length"), token.length()); + + // Update OTP token length + performServerPut(requestSpecWithTFA, responseSpec, "/fineract-provider/api/v1/twofactor/configure?" + TENANT_IDENTIFIER, + "{\"otp-token-length\": 10 }", ""); + + // Invalidate token for re-login + performServerPost(requestSpecWithTFA, responseSpec, "/fineract-provider/api/v1/twofactor/invalidate?" + TENANT_IDENTIFIER, + "{ \"token\": \"" + tfaToken + "\" }", ""); + + // Login again + performServerPost(requestSpec, responseSpec, + "/fineract-provider/api/v1/twofactor?deliveryMethod=email&extendedToken=false&" + TENANT_IDENTIFIER, "", ""); + assertEquals(greenMail.getReceivedMessages().length, 2); + + Matcher m2 = p.matcher((CharSequence) greenMail.getReceivedMessages()[1].getContent()); + + String token2 = null; + + while (m2.find()) { + token2 = m2.group(1); + } + + assertNotNull(token2); + + // Check that the configuration has worked and length is now 10 + assertEquals(10, token2.length()); + + tfaToken = performServerPost(requestSpec, responseSpec, + "/fineract-provider/api/v1/twofactor/validate?token=" + token2 + "&" + TENANT_IDENTIFIER, "", "token"); + assertNotNull(tfaToken); + + requestSpecWithTFA = new RequestSpecBuilder() // + .setContentType(ContentType.JSON) // + .addHeader("Fineract-Platform-TFA-Token", tfaToken) // + .addHeader("Authorization", "Basic " + basicAuthenticationKey) // + .build(); + + // Get the configuration and check one of the values (OTP token length) + json = performServerGet(requestSpecWithTFA, responseSpec, "/fineract-provider/api/v1/twofactor/configure?" + TENANT_IDENTIFIER, ""); + assertEquals(json.get("otp-token-length"), token2.length()); + + // Update OTP token length back to original value + performServerPut(requestSpecWithTFA, responseSpec, "/fineract-provider/api/v1/twofactor/configure?" + TENANT_IDENTIFIER, + "{\"otp-token-length\": " + token.length() + "}", ""); + + // Check that configuration has been reset + json = performServerGet(requestSpecWithTFA, responseSpec, "/fineract-provider/api/v1/twofactor/configure?" + TENANT_IDENTIFIER, ""); + assertEquals(json.get("otp-token-length"), token.length()); + + // Invalidate token for re-login + performServerPost(requestSpecWithTFA, responseSpec, "/fineract-provider/api/v1/twofactor/invalidate?" + TENANT_IDENTIFIER, + "{ \"token\": \"" + tfaToken + "\" }", ""); + } + + private static void initializeRestAssured() { + RestAssured.baseURI = "https://localhost"; + RestAssured.port = 8443; + RestAssured.keyStore("src/main/resources/keystore.jks", "openmf"); + RestAssured.useRelaxedHTTPSValidation(); + } + + private static void awaitSpringBootActuatorHealthyUp() throws InterruptedException { + int attempt = 0; + final int max_attempts = 10; + Response response = null; + + do { + try { + response = RestAssured.get(HEALTH_URL); + + if (response.statusCode() == 200) { + return; + } + + Thread.sleep(3000); + } catch (Exception e) { + Thread.sleep(3000); + } + } while (attempt < max_attempts); + + fail(HEALTH_URL + " returned " + response.prettyPrint()); + } + + @SuppressWarnings("unchecked") + private static T performServerGet(final RequestSpecification requestSpec, final ResponseSpecification responseSpec, + final String getURL, final String jsonAttributeToGetBack) { + final String json = given().spec(requestSpec).expect().spec(responseSpec).log().ifError().when().get(getURL).andReturn().asString(); + if (jsonAttributeToGetBack == null) { + return (T) json; + } + return (T) JsonPath.from(json).get(jsonAttributeToGetBack); + } + + @SuppressWarnings("unchecked") + private static T performServerPost(final RequestSpecification requestSpec, final ResponseSpecification responseSpec, + final String postURL, final String jsonBodyToSend, final String jsonAttributeToGetBack) { + final String json = given().spec(requestSpec).body(jsonBodyToSend).expect().spec(responseSpec).log().ifError().when().post(postURL) + .andReturn().asString(); + if (jsonAttributeToGetBack == null) { + return (T) json; + } + return (T) JsonPath.from(json).get(jsonAttributeToGetBack); + } + + @SuppressWarnings("unchecked") + public static T performServerPut(final RequestSpecification requestSpec, final ResponseSpecification responseSpec, + final String putURL, final String jsonBodyToSend, final String jsonAttributeToGetBack) { + final String json = given().spec(requestSpec).body(jsonBodyToSend).expect().spec(responseSpec).log().ifError().when().put(putURL) + .andReturn().asString(); + return (T) JsonPath.from(json).get(jsonAttributeToGetBack); + } +}